diff --git a/LICENSE.txt b/LICENSE.txt
index 5ea3ea0..0f70ddf 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,5 +1,10 @@
-"animotes.py" and its contents copyright (C) 2017 Valentijn <ev1l0rd>
+"animotes.py" and its contents copyright (C) 2017 Valentijn <ev1l0rd> and DiamondIceNS
 and distributed under the GNU Public License 3.0 as described in GPL3.txt.
 
+"lzstring.py" and its contents copyright (C) 2017 Marcel Dancak and distributed
+under the Do What The Fuck You Want To Public License as described in WTFPL2.txt.
+
 All other code is authored by DiamondIceNS and is distributed under the
 Creative Commons Public Domain Dedication 0 as described in CCO.txt.
+
+Copies of these licenses can be found in the `licenses` directory.
diff --git a/README.md b/README.md
index 218cb1b..02446a9 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,17 @@
+<img align="right" width="200" height="200" src="avatar.jpg">
+
 # Starlight Glimmer
 A template utility bot based on [Alastair](Make-Alastair-Great-Again) and [Pinkie Pie](https://pastebin.com/Tg1p5AnW).
 
-Currently supports both [Pixelcanvas.io](http://pixelcanvas.io/) and [Pixelz.io](http://pixelz.io/).
+Currently supports [Pixelcanvas.io](http://pixelcanvas.io/), [Pixelz.io](http://pixelz.io/), and [Pixelzone.io](http://pixelzone.io/).
 
 #### Requires:
 - Python 3.5
 - [Discord.py rewrite](https://github.com/Rapptz/discord.py/tree/rewrite)
 - [Pillow](https://pillow.readthedocs.io/en/latest/installation.html)
+- [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
+- [socketIO_client](https://github.com/invisibleroads/socketIO-client)
+- [lz4](https://github.com/python-lz4/python-lz4)
 
 #### Installation:
 1. Install Python and the required libraries
@@ -15,10 +20,18 @@ Currently supports both [Pixelcanvas.io](http://pixelcanvas.io/) and [Pixelz.io]
 4. `python glimmer.py`
 
 #### Features:
-- Live canvas preview
-- Live template checking
-- Above two features can be toggled to trigger any time a link/valid template is posted (instead of requiring a command)
+- Automatic live canvas preview
+- Automatic live template checking
+- Color quantization of templates to canvas palette
 - [Animotes](https://github.com/ev1l0rd/animotes) support, just because
-- Use of cross-server emoji through Animotes (must opt-in)
+- Cross-guild emoji through Animotes (must opt-in)
+- Support for full language localization
+
+### Languages:
+- English (US)
+
+If you happen to know a language that is not listed and would be willing to translate, please submit a pull request. (French and Brazilian Portuguese are specifically wanted, but no translation is a worthless translation!)
+
+**Invite:** `https://discordapp.com/oauth2/authorize?&client_id=405480380930588682&scope=bot&permissions=35840`
 
-**Invite:** `https://discordapp.com/oauth2/authorize?&client_id=405480380930588682&scope=bot&permissions=35840`
\ No newline at end of file
+[avatar]: avatar.jpg
\ No newline at end of file
diff --git a/avatar.jpg b/avatar.jpg
new file mode 100644
index 0000000..20925ad
Binary files /dev/null and b/avatar.jpg differ
diff --git a/commands/animotes.py b/commands/animotes.py
index 7e0f960..6f11880 100644
--- a/commands/animotes.py
+++ b/commands/animotes.py
@@ -1,14 +1,16 @@
+import re
 import discord
 from discord.ext import commands
-import re
 
+import utils.sqlite as sql
 from utils.channel_logger import ChannelLogger
-from utils.logger import Log
 from utils.exceptions import NoPermission
-import utils.sqlite as sql
+from utils.language import getlang
+from utils.logger import Log
 
-#    Cog to reformat messages to allow for animated emotes, regardless of nitro status.
-#    Copyright (C) 2017 Valentijn <ev1l0rd>
+#    Cog to reformat messages to allow for animated emotes, regardless of nitro status
+#    and sharing those emotes with other servers with opt-in policy.
+#    Copyright (C) 2017-2018 Valentijn <ev1l0rd> and DiamondIceNS
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
@@ -40,45 +42,17 @@ async def on_message(self, message):
 
     @commands.command(aliases=['unregister'])
     async def register(self, ctx):
-        """Toggle your opt-in status to allow this bot to replace custom emoji on your behalf.
-
-        If you opt-in to this feature, any messages you send that contain emoji that I can see will be deleted
-        and replaced with an identical message from the bot with that emoji properly rendered. Bots do not require
-        a Nitro account to access cross-server or animated emoji, so you can use this to crudely work around that
-        restriction if you do not have Nitro yourself. It's the poor man's Nitro!
-
-        If your server has been opted in to emoji sharing, you can use this feature to steal any emoji from any server
-        this bot happens to be in, provided that other server has also opted in.
-
-        Use unregister to opt out after opting in.
-
-        This feature will malfunction if you have Nitro and you use your own animated emoji in the same message. Trying
-        to use a Twitch integration server emoji in the same message will probably also break this feature.
-
-        This feature requires me having the Manage Messages permission."""
         if not sql.is_user_animote_user(ctx.author.id):
             sql.add_animote_user(ctx.author.id)
-            message = "Successfully opted in to animated emote conversion."
+            message = getlang(ctx.guild.id, "animotes.member_opt_in")
         else:
             sql.delete_animote_user(ctx.author.id)
-            message = "Successfully opted out of animated emote conversion."
+            message = getlang(ctx.guild.id, "animotes.member_opt_out")
         await ctx.message.author.send(content=message)
 
     @commands.command(aliases=['unregisterserver'])
     @commands.guild_only()
-    async def registerserver(self, ctx):
-        """Toggle the server's opt-in status to emoji sharing.
-
-        Opting in to emoji sharing gives you access to all the emoji I can see in every other server I am in, as long
-        as those servers have also opted in to emoji sharing. This also means that opting in gives those servers access
-        to YOUR emoji as well. You do not need to opt in to this feature to enable animated emoji replacement for emoji
-        unique to this server.
-
-        WARNING: Opting in to this feature will let other servers see all your custom emoji as well as your server's
-        name and, by extension, your server's ID. If you do not want to share your server's presence with others, DO
-        NOT opt in to this feature!
-
-        This command can only be used by members with the Manage Emojis permission."""
+    async def registerguild(self, ctx):
         if not ctx.author.permissions_in(ctx.channel).manage_emojis:
             raise NoPermission
         if not sql.is_server_emojishare_server(ctx.guild.id):
@@ -86,18 +60,17 @@ async def registerserver(self, ctx):
             self.log.info("Guild {0.name} (ID: {0.id}) has opted in to emoji sharing.".format(ctx.guild))
             await self.channel_logger.log_to_channel("Guild **{0.name}** (ID: `{0.id}`) has opted in to emoji sharing."
                                                 .format(ctx.guild))
-            message = "Successfully opted server in to emoji sharing."
+            message = getlang(ctx.guild.id, "animotes.guild_opt_in")
         else:
             sql.update_guild(ctx.guild.id, emojishare=0)
             self.log.info("Guild {0.name} (ID: {0.id}) has opted out of emoji sharing.".format(ctx.guild))
             await self.channel_logger.log_to_channel("Guild **{0.name}** (ID: `{0.id}`) has opted out of emoji sharing."
                                                 .format(ctx.guild))
-            message = "Successfully opted server out of emoji sharing."
+            message = getlang(ctx.guild.id, "animotes.guild_opt_out")
         await ctx.send(message)
 
     @commands.command()
     async def listemotes(self, ctx):
-        """Lists out all of the animated emotes that I know about"""
         guilds = []
         blacklist = []
         whitelist = []
diff --git a/commands/canvas.py b/commands/canvas.py
new file mode 100644
index 0000000..c7ed204
--- /dev/null
+++ b/commands/canvas.py
@@ -0,0 +1,259 @@
+import re
+from discord.ext import commands
+from utils.language import getlang
+
+import utils.render as render
+import utils.sqlite as sql
+from utils.colors import pzone_colors, pzio_colors, pc_colors
+from utils.logger import Log
+
+log = Log(__name__)
+
+
+class Canvas:
+    def __init__(self, bot):
+        self.bot = bot
+
+    # =======================
+    #          DIFF
+    # =======================
+
+    @commands.group(name="diff")
+    async def diff(self, ctx):
+        pass
+
+    @staticmethod
+    async def parse_diff(ctx, coords):
+        if len(ctx.message.attachments) < 1:
+            await ctx.send(getlang(ctx.guild.id, "bot.error.missing_attachment"))
+            return
+        filename = ctx.message.attachments[0].filename
+        if filename[-4:].lower() != ".png":
+            if filename[-4:].lower() == ".jpg" or filename[-5:].lower() == ".jpeg":
+                await ctx.send(getlang(ctx.guild.id, "bot.error.jpeg"))
+                return
+            await ctx.send(getlang(ctx.guild.id, "bot.error.no_png"))
+            return
+        m = re.search('\(?(-?\d+), ?(-?\d+)\)?\s?#?(\d+)?', coords)
+        if m is not None:
+            x = int(m.group(1))
+            y = int(m.group(2))
+            att = ctx.message.attachments[0]
+            zoom = int(m.group(3)) if m.group(3) is not None else 1
+            zoom = max(1, min(zoom, 400 // att.width, 400 // att.height))
+            return ctx, x, y, att, zoom
+
+    @diff.command(name="pixelcanvas")
+    async def diff_pixelcanvas(self, ctx, *, coordinates: str):
+        args = await Canvas.parse_diff(ctx, coordinates)
+        if args is not None:
+            log.debug("Pixelcanvas diff invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.diff(*args, render.fetch_pixelcanvas, pc_colors)
+
+    @diff.command(name="pixelzio")
+    async def diff_pixelzio(self, ctx, *, coordinates: str):
+        args = await Canvas.parse_diff(ctx, coordinates)
+        if args is not None:
+            log.debug("Pixelzio diff invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.diff(*args, render.fetch_pixelzio, pzio_colors)
+
+    @diff.command(name="pixelzone")
+    async def diff_pixelzone(self, ctx, *, coordinates: str):
+        args = await Canvas.parse_diff(ctx, coordinates)
+        if args is not None:
+            log.debug("Pixelzone diff invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.SIOConn().diff(*args)
+
+    # =======================
+    #        PREVIEW
+    # =======================
+
+    @commands.group(name="preview")
+    async def preview(self, ctx):
+        pass
+
+    @staticmethod
+    async def parse_preview(ctx, coords):
+        m = re.search('(-?\d+), ?(-?\d+)/?\s?#?(\d+)?', coords)
+        if m is not None:
+            x = int(m.group(1))
+            y = int(m.group(2))
+            zoom = int(m.group(3)) if m.group(3) is not None else 1
+            zoom = max(min(zoom, 16), 1)
+            return ctx, x, y, zoom
+
+    @preview.command(name="pixelcanvas")
+    async def preview_pixelcanvas(self, ctx, *, coordinates: str):
+        args = await Canvas.parse_preview(ctx, coordinates)
+        if args is not None:
+            log.debug("Pixelcanvas preview invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.preview(*args, render.fetch_pixelcanvas)
+
+    @preview.command(name="pixelzio")
+    async def preview_pixelzio(self, ctx, *, coordinates: str):
+        args = await Canvas.parse_preview(ctx, coordinates)
+        if args is not None:
+            log.debug("Pixelzio preview invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.preview(*args, render.fetch_pixelzio)
+
+    @preview.command(name="pixelzone")
+    async def preview_pixelzone(self, ctx, *, coordinates: str):
+        args = await Canvas.parse_preview(ctx, coordinates)
+        if args is not None:
+            log.debug("Pixelzone preview invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.SIOConn().preview(*args)
+
+    # =======================
+    #        QUANTIZE
+    # =======================
+
+    @commands.group(name="quantize")
+    async def quantize(self, ctx):
+        pass
+
+    @staticmethod
+    async def check_attachment(ctx):
+        if len(ctx.message.attachments) < 1:
+            await ctx.send(getlang(ctx.guild.id, "bot.error.missing_attachment"))
+            return False
+        filename = ctx.message.attachments[0].filename
+        if filename[-4:].lower() != ".png":
+            if filename[-4:].lower() == ".jpg" or filename[-5:].lower() == ".jpeg":
+                await ctx.send(getlang(ctx.guild.id, "bot.error.jpeg"))
+                return False
+            await ctx.send(getlang(ctx.guild.id, "bot.error.no_png"))
+            return False
+        return True
+
+    @quantize.command(name="pixelcanvas")
+    async def quantize_pixelcanvas(self, ctx):
+        if await Canvas.check_attachment(ctx):
+            log.debug("Pixelcanvas quantize invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.quantize(ctx, ctx.message.attachments[0], pc_colors)
+
+    @quantize.command(name="pixelzio")
+    async def quantize_pixelzio(self, ctx):
+        if await Canvas.check_attachment(ctx):
+            log.debug("Pixelzio quantize invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.quantize(ctx, ctx.message.attachments[0], pzio_colors)
+
+    @quantize.command(name="pixelzone")
+    async def quantize_pixelzone(self, ctx):
+        if await Canvas.check_attachment(ctx):
+            log.debug("Pixelzone quantize invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
+                      .format(ctx.author, ctx.guild))
+            await render.quantize(ctx, ctx.message.attachments[0], pzone_colors)
+
+    @commands.command()
+    async def repeat(self, ctx):
+        async for msg in ctx.history(limit=50, before=ctx.message):
+            regex = ctx.prefix \
+                    + '(diff|preview) (pixelcanvas|pixelzio|pixelzone)(?: (-?\d+), ?(-?\d+)/?\s?#?(\d+)?)?'
+            match = re.search(regex, msg.content)
+            if match:
+                cmd = match.group(1)
+                sub_cmd = match.group(2)
+                x = int(match.group(3))
+                y = int(match.group(4))
+                zoom = int(match.group(5)) if match.group(5) is not None else 1
+                if cmd == "diff" and len(msg.attachments) > 0 and msg.attachments[0].filename[-4:].lower() == ".png":
+                    att = msg.attachments[0]
+                    zoom = max(1, min(zoom, 400 // att.width, 400 // att.height))
+                    if sub_cmd == "pixelcanvas":
+                        await render.diff(ctx, x, y, att, zoom, render.fetch_pixelcanvas, pc_colors)
+                        return
+                    elif sub_cmd == "pixelzio":
+                        await render.diff(ctx, x, y, att, zoom, render.fetch_pixelzio, pzio_colors)
+                        return
+                    elif sub_cmd == "pixelzone":
+                        await render.SIOConn().diff(ctx, x, y, att, zoom)
+                        return
+                if cmd == "preview":
+                    zoom = max(1, min(16, zoom))
+                    if sub_cmd == "pixelcanvas":
+                        await render.preview(ctx, x, y, zoom, render.fetch_pixelcanvas)
+                        return
+                    elif sub_cmd == "pixelzio":
+                        await render.preview(ctx, x, y, zoom, render.fetch_pixelzio)
+                        return
+                    elif sub_cmd == "pixelzone":
+                        await render.SIOConn().preview(ctx, x, y, zoom)
+                        return
+
+            default_canvas = sql.select_guild_by_id(ctx.guild.id)['default_canvas']
+            pc_match = re.search('(?:pixelcanvas\.io/)@(-?\d+),(-?\d+)/?(?:\s?#?(\d+))?', msg.content)
+            pzio_match = re.search('(?:pixelz\.io/)@(-?\d+),(-?\d+)(?:\s?#?(\d+))?', msg.content)
+            pzone_match = re.search('(?:pixelzone\.io/)\?p=(-?\d+),(-?\d+)(?:,(\d+))?(?:\s?#?(\d+))?', msg.content)
+            prev_match = re.search('@\(?(-?\d+), ?(-?\d+)\)?(?: ?#(\d+))?', msg.content)
+            diff_match = re.search('\(?(-?\d+), ?(-?\d+)\)?(?: ?#(\d+))?', msg.content)
+
+            if pc_match is not None:
+                x = int(pc_match.group(1))
+                y = int(pc_match.group(2))
+                zoom = int(pc_match.group(3)) if pc_match.group(3) is not None else 1
+                zoom = max(min(zoom, 16), 1)
+                await render.preview(ctx, x, y, zoom, render.fetch_pixelcanvas)
+                return
+
+            if pzio_match is not None:
+                x = int(pzio_match.group(1))
+                y = int(pzio_match.group(2))
+                zoom = int(pzio_match.group(3)) if pzio_match.group(3) is not None else 1
+                zoom = max(min(zoom, 16), 1)
+                await render.preview(ctx, x, y, zoom, render.fetch_pixelzio)
+                return
+
+            if pzone_match is not None:
+                x = int(pzone_match.group(1))
+                y = int(pzone_match.group(2))
+                if pzone_match.group(4) is not None:
+                    zoom = int(pzone_match.group(4))
+                elif pzone_match.group(3) is not None:
+                    zoom = int(pzio_match.group(3))
+                else:
+                    zoom = 1
+                zoom = max(min(zoom, 16), 1)
+                await render.SIOConn().preview(ctx, x, y, zoom)
+                return
+
+            if prev_match is not None:
+                x = int(prev_match.group(1))
+                y = int(prev_match.group(2))
+                zoom = int(prev_match.group(3)) if prev_match.group(3) is not None else 1
+                zoom = max(min(zoom, 16), 1)
+                if default_canvas == "pixelcanvas.io":
+                    await render.preview(ctx, x, y, zoom, render.fetch_pixelcanvas)
+                elif default_canvas == "pixelz.io":
+                    await render.preview(ctx, x, y, zoom, render.fetch_pixelzio)
+                elif default_canvas == "pixelzone.io":
+                    await render.SIOConn().preview(ctx, x, y, zoom)
+                return
+
+            if diff_match is not None and len(msg.attachments) > 0 \
+                    and msg.attachments[0].filename[-4:].lower() == ".png":
+                att = msg.attachments[0]
+                x = int(diff_match.group(1))
+                y = int(diff_match.group(2))
+                zoom = int(diff_match.group(3)) if diff_match.group(3) is not None else 1
+                zoom = max(1, min(zoom, 400 // att.width, 400 // att.height))
+                if default_canvas == "pixelcanvas.io":
+                    await render.diff(ctx, x, y, att, zoom, render.fetch_pixelcanvas, pc_colors)
+                elif default_canvas == "pixelz.io":
+                    await render.diff(ctx, x, y, att, zoom, render.fetch_pixelzio, pzio_colors)
+                elif default_canvas == "pixelzone.io":
+                    await render.SIOConn().diff(ctx, x, y, att, zoom)
+                return
+
+            ctx.send(getlang(ctx.guild.id, "render.repeat_not_found"))
+
+
+def setup(bot):
+    bot.add_cog(Canvas(bot))
diff --git a/commands/configuration.py b/commands/configuration.py
index f484a8d..4d569cb 100644
--- a/commands/configuration.py
+++ b/commands/configuration.py
@@ -1,7 +1,8 @@
-from discord.ext import commands
 from discord import TextChannel
+from discord.ext import commands
 
 from utils.exceptions import NoPermission
+from utils.language import getlang
 import utils.sqlite as sql
 
 
@@ -9,104 +10,77 @@ class Configuration:
     def __init__(self, bot):
         self.bot = bot
 
-    @commands.command()
+    @commands.group(name="alertchannel")
     @commands.guild_only()
-    async def setalertchannel(self, ctx, channel: TextChannel):
-        """Sets the preferred channel for update alerts.
+    async def alertchannel(self, ctx):
+        pass
 
-        If set, I will post there every time I update so you know when new features are ready.
-        Use the #channel syntax to ensure the correct channel is selected if multiple channels have the same name.
-        I need permission to post in the specified channel for this to have effect.
-
-        Only users with the Administrator role can use this command."""
+    @alertchannel.command(name="set")
+    @commands.guild_only()
+    async def alertchannel_set(self, ctx, channel: TextChannel):
         if not ctx.author.permissions_in(ctx.channel).administrator:
             raise NoPermission
         sql.update_guild(ctx.guild.id, alert_channel=channel.id)
-        await ctx.send("Alert channel set!")
+        await ctx.send(getlang(ctx.guild.id, "configuration.alert_channel_set").format(channel.mention))
 
-    @commands.command()
+    @alertchannel.command(name="clear")
     @commands.guild_only()
-    async def clearalertchannel(self, ctx):
-        """Clears the preferred channel for update alerts.
-
-        If this is cleared, I will no longer alert this server when the update version has changed.
-
-        Only users with the Administrator role can use this command."""
+    async def alertchannel_clear(self, ctx):
         if not ctx.author.permissions_in(ctx.channel).administrator:
             raise NoPermission
         sql.update_guild(ctx.guild.id, alert_channel=0)
-        await ctx.send("Alert channel cleared!")
+        await ctx.send(getlang(ctx.guild.id, "configuration.alert_channel_cleared"))
 
     @commands.command()
     @commands.guild_only()
     async def setprefix(self, ctx, prefix):
-        """Sets my command prefix for this server.
-
-        The prefix is the substring one types immediately before any command to use that command. Set it to something
-        short that doesn't conflict with another bot on the server! Maximum 10 characters. You really shouldn't need
-        anything longer. Default prefix is 'g!'.
-
-        Only users with the Administrator role can use this command."""
         if not ctx.author.permissions_in(ctx.channel).administrator:
             raise NoPermission
         if len(prefix) > 10:
             raise commands.BadArgument
         sql.update_guild(ctx.guild.id, prefix=prefix)
-        await ctx.send("Prefix for this server has been set to `{}`".format(prefix))
+        await ctx.send(getlang(ctx.guild.id, "configuration.prefix_set").format(prefix))
 
     @commands.command()
     @commands.guild_only()
     async def autoscan(self, ctx):
-        """Toggles whether I will automatically scan all messages for preview or diff requests.
-
-        If this is on, any post that contains valid coordinate pairs will be treated as either an invocation of
-        the 'preview' command or 'diff' command for one of the supported canvas websites. If a full URL is posted, the
-        canvas in the URL will be used. If only a coordinate pair is posted, the default canvas will be used.
-
-        'diff' will only fire if the same message also contains an image attachment. 'preview' will take precedence over
-        'diff' if the coordinates are preceeded by the '@' character, even if an attachment is provided.
-
-        See each command's help page for a given canvas for more information on the syntax for each.
-        See `setdefaultcanvas` for more info on setting a default canvas.
-
-        Only users with the Administrator role can use this command."""
         if not ctx.author.permissions_in(ctx.channel).administrator:
             raise NoPermission
         if sql.select_guild_by_id(ctx.guild.id)['autoscan'] == 0:
             sql.update_guild(ctx.guild.id, autoscan=1)
-            await ctx.send("Autoscan has been enabled.")
+            await ctx.send(getlang(ctx.guild.id, "configuration.autoscan_enabled"))
         else:
             sql.update_guild(ctx.guild.id, autoscan=0)
-            await ctx.send("Autoscan has been disabled.")
+            await ctx.send(getlang(ctx.guild.id, "configuration.autoscan_disabled"))
 
-    @commands.command()
+    @commands.group(name="setdefaultcanvas")
     @commands.guild_only()
-    async def setdefaultcanvas(self, ctx, *args):
-        """Sets the default canvas used for autoscan.
+    async def setdefaultcanvas(self, ctx):
+        pass  # TODO
 
-        Valid canvas options:
-        - pixelcanvas.io
-        - pixelz.io
+    @setdefaultcanvas.command(name="pixelcanvas")
+    @commands.guild_only()
+    async def setdefaultcanvas_pixelcanvas(self, ctx):
+        if not ctx.author.permissions_in(ctx.channel).administrator:
+            raise NoPermission
+        sql.update_guild(ctx.guild.id, default_canvas="pixelcanvas.io")
+        await ctx.send(getlang(ctx.guild.id, "configuration.default_canvas_set").format("Pixelcanvas.io"))
 
-        Use this command without an argument to see which canvas is currently set. Default is pixelcanvas.io.
-        See the 'autoscan' command for more information.
+    @setdefaultcanvas.command(name="pixelzio")
+    @commands.guild_only()
+    async def setdefaultcanvas_pixelzio(self, ctx):
+        if not ctx.author.permissions_in(ctx.channel).administrator:
+            raise NoPermission
+        sql.update_guild(ctx.guild.id, default_canvas="pixelz.io")
+        await ctx.send(getlang(ctx.guild.id, "configuration.default_canvas_set").format("Pixelz.io"))
 
-        Only users with the Administrator role can use this command."""
+    @setdefaultcanvas.command(name="pixelzone")
+    @commands.guild_only()
+    async def setdefaultcanvas_pixelzone(self, ctx):
         if not ctx.author.permissions_in(ctx.channel).administrator:
             raise NoPermission
-        canvas = args[0] if len(args) > 0 else None
-        if canvas is None or canvas == "":
-            g = sql.select_guild_by_id(ctx.guild.id)
-            await ctx.send("Current default canvas for this server is **{0}**".format(g['default_canvas']))
-            return
-        if canvas == "pixelcanvas.io":
-            sql.update_guild(ctx.guild.id, default_canvas="pixelcanvas.io")
-            await ctx.send("Default canvas set to **pixelcanvas.io**")
-        elif canvas == "pixelz.io":
-            sql.update_guild(ctx.guild.id, default_canvas="pixelz.io")
-            await ctx.send("Default canvas set to **pixelz.io**")
-        else:
-            raise commands.BadArgument
+        sql.update_guild(ctx.guild.id, default_canvas="pixelzone.io")
+        await ctx.send(getlang(ctx.guild.id, "configuration.default_canvas_set").format("Pixelzone.io"))
 
 
 def setup(bot):
diff --git a/commands/pixelcanvas.py b/commands/pixelcanvas.py
deleted file mode 100644
index 5e4486d..0000000
--- a/commands/pixelcanvas.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import re
-from discord.ext import commands
-
-from utils.render import pixelcanvasio_preview, pixelcanvasio_diff
-
-
-class Pixelcanvas:
-    def __init__(self, bot):
-        self.bot = bot
-
-    @commands.command()
-    async def pcdiff(self, ctx, *, coordinates: str):
-        """Takes an uploaded template and checks the canvas to see how complete it is.
-        Add -e at the end to check against the experimental canvas.
-
-        If autoscan is enabled and Pixelcanvas is set to your default canvas, you do not need to explicitly invoke this
-        command -- any valid coordinates in the same message as a attachment will trigger this command automatically.
-        See help for 'autoscan' and 'setdefaultcanvas' commands for more information.
-
-        Usage examples (with uploaded attachment):
-        - 0,0
-        - 0, 0
-        - (0, 0) -e
-        """
-        if len(ctx.message.attachments) < 1:
-            await ctx.send("That command requires an attached template to check against.")
-            return
-        m = re.search('\(?(-?\d+), ?(-?\d+)\)?(?: (-e)?)?', coordinates)
-        if m is not None:
-            x = int(m.group(1))
-            y = int(m.group(2))
-            att = ctx.message.attachments[0]
-            is_exp = m.group(3) is not None
-            await pixelcanvasio_diff(ctx, x, y, att, is_exp)
-
-    @commands.command()
-    async def pcpreview(self, ctx, *, coordinates: str):
-        """Render a preview of the canvas centered at the given url/coordinates.
-        Add a number like #2 to the end of the url/coordinates to zoom the preview by the corresponding factor. (Max 16)
-        Add the experimental subdomain to the URL or add -e at the end to render on the experimental canvas.
-
-        If autoscan is enabled and Pixelcanvas is set to your default canvas, you do not need to explicitly invoke this
-        command -- any message containing coordinates prefixed with '@' will trigger this command automatically.
-        See help for 'autoscan' and 'setdefaultcanvas' commands for more information.
-
-        Usage examples:
-        - http://pixelcanvas.io/@0,0
-        - pixelcanvas.io/@0,0 #2
-        - experimental.pixelcanvas.io/@0,0 #8
-        - @0, 0
-        - @0, 0 #4 -e
-        """
-        m = re.search('(?:(experimental)?\.pixelcanvas\.io/)?@(-?\d+), ?(-?\d+)/?\s?#?([248])?(?:\d+)?(?: ?(-e))?',
-                      coordinates)
-        if m is not None:
-            x = int(m.group(2))
-            y = int(m.group(3))
-            zoom = int(m.group(4)) if m.group(4) is not None else 1
-            is_exp = m.group(1) is not None or m.group(5) is not None
-            await pixelcanvasio_preview(ctx, x, y, zoom, is_exp)
-
-
-def setup(bot):
-    bot.add_cog(Pixelcanvas(bot))
diff --git a/commands/pixelzio.py b/commands/pixelzio.py
deleted file mode 100644
index 4a4583c..0000000
--- a/commands/pixelzio.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import re
-from discord.ext import commands
-
-from utils.render import *
-
-
-class Pixelzio:
-    def __init__(self, bot):
-        self.bot = bot
-
-    @commands.command()
-    async def pziodiff(self, ctx, *, coordinates: str):
-        """Takes an uploaded template and checks the canvas to see how complete it is.
-
-        If autoscan is enabled and Pixelz.io is set as your default canvas, you do not need to explicitly invoke this
-        command -- any valid coordinates in the same message as a attachment will trigger this command automatically.
-        See help for 'autoscan' and 'setdefaultcanvas' commands for more information.
-
-        Usage examples (with uploaded attachment):
-        - 0,0
-        - 0, 0
-        - (0, 0) -e
-        """
-        if len(ctx.message.attachments) < 1:
-            await ctx.send("That command requires an attached template to check against.")
-            return
-        m = re.search('\(?(-?\d+), ?(-?\d+)\)?', coordinates)
-        if m is not None:
-            x = int(m.group(1))
-            y = int(m.group(2))
-            att = ctx.message.attachments[0]
-            await pixelzio_diff(ctx, x, y, att)
-
-    @commands.command()
-    async def pziopreview(self, ctx, *, coordinates: str):
-        """Render a preview of the canvas centered at the given url/coordinates.
-        Add a number like #2 to the end of the url/coordinates to zoom the preview by the corresponding factor. (Max 16)
-
-        If autoscan is enabled and Pixelz.io is set to your default canvas, you do not need to explicitly invoke this
-        command -- any message containing coordinates prefixed with '@' will trigger this command automatically.
-        See help for 'autoscan' and 'setdefaultcanvas' commands for more information.
-
-        Usage examples:
-        - http://pixelz.io/@0,0
-        - pixelz.io/@0,0 #2
-        - @0, 0
-        """
-        m = re.search('@(-?\d+), ?(-?\d+)/?\s?#?(\d+)?', coordinates)
-        if m is not None:
-            x = int(m.group(1))
-            y = int(m.group(2))
-            zoom = int(m.group(3)) if m.group(3) is not None else 1
-            zoom = max(min(zoom, 16), 1)
-            await pixelzio_preview(ctx, x, y, zoom)
-
-
-def setup(bot):
-    bot.add_cog(Pixelzio(bot))
diff --git a/config/config.json.example b/config/config.json.example
index 33c6f90..0202b83 100644
--- a/config/config.json.example
+++ b/config/config.json.example
@@ -1,6 +1,7 @@
 {
     "token": "your-token-here",
     "prefix": "g!",
+    "name": "Starlight Glimmer",
 
     "preview_height": 240,
     "preview_width": 400,
diff --git a/glimmer.py b/glimmer.py
index 726f33f..344aea8 100644
--- a/glimmer.py
+++ b/glimmer.py
@@ -1,15 +1,20 @@
+import discord
 import re
-from discord.ext import commands
+import traceback
 from discord import TextChannel
+from discord.ext import commands
 from time import time
 
+import utils.render as render
+import utils.sqlite as sql
 from utils.channel_logger import ChannelLogger
+from utils.colors import *
 from utils.config import Config
+from utils.exceptions import NoPermission
+from utils.language import getlang
 from utils.logger import Log
-from utils.render import *
+from utils.help_formatter import GlimmerHelpFormatter
 from utils.version import VERSION
-from utils.exceptions import *
-import utils.sqlite as sql
 
 
 def get_prefix(bot, msg):
@@ -21,38 +26,31 @@ def get_prefix(bot, msg):
 
 
 cfg = Config()
-log = Log('StarlightGlimmer')
-description = """
-Hi! I'm a Pixelcanvas.io helper bot!
-
-If you ever post a link to a supported pixel-placing website, I'll send you a preview of the spot you linked to.
-Also, if you upload a PNG template and give me the coordinates of its top left corner, I'll show you the pixels that
-don't match and tell you how many mistakes there are.
-
-Lastly, I answer to the following commands:
-"""
-bot = commands.Bot(command_prefix=get_prefix, description=description)
+log = Log(''.join(cfg.name.split()))
+bot = commands.Bot(command_prefix=get_prefix, formatter=GlimmerHelpFormatter())
 channel_logger = ChannelLogger(bot)
-
 extensions = [
-    "commands.pixelcanvas",
-    "commands.pixelzio",
-    "commands.configuration",
-    "commands.animotes"
+    "commands.animotes",
+    "commands.canvas",
+    "commands.configuration"
 ]
 
 
 @bot.event
 async def on_ready():
     log.info("Performing guild check...")
-    new_ver_alert = sql.get_version() != VERSION
-    if new_ver_alert:
-        sql.update_version(VERSION)
+    if sql.get_version() is None:
+        sql.init_version(VERSION)
+        new_ver_alert = False
+    else:
+        new_ver_alert = sql.get_version() != VERSION and sql.get_version() is not None
+        if new_ver_alert:
+            sql.update_version(VERSION)
     for g in bot.guilds:
         log.info("Servicing guild '{0.name}' (ID: {0.id})".format(g))
         row = sql.select_guild_by_id(g.id)
         if row is not None:
-            prefix = row['prefix'] if row['prefix'] is not None else "g!"
+            prefix = row['prefix'] if row['prefix'] is not None else cfg.prefix
             if g.name != row['name']:
                 await channel_logger.log_to_channel("Guild ID `{0.id}` changed name from **{1}** to **{0.name}** since "
                                                     "last bot start".format(g, row['name']))
@@ -61,10 +59,8 @@ async def on_ready():
             if new_ver_alert and alert_channel_id is not 0:
                 alert_channel = next((x for x in g.channels if x.id == alert_channel_id), None)
                 if alert_channel is not None:
-                    await alert_channel.send("This bot has updated to version **{}**! Check out the command help page "
-                                             "for new commands with `{}help`, or visit https://github.com/DiamondIceNS/"
-                                             "StarlightGlimmer/releases for the full changelog.".format(VERSION,
-                                                                                                        prefix))
+                    await alert_channel.send(getlang(g.id, "bot.alert_update").format(VERSION, prefix))
+                    log.info("Sent update message to guild {0.name} (ID: {0.id})")
                 else:
                     log.info("Could not send update message to guild {0.name} (ID: {0.id}): "
                              "Alert channel could not be found.")
@@ -96,12 +92,12 @@ async def on_ready():
 async def print_welcome_message(guild):
     c = next((x for x in guild.channels if x.name == "general" and x.permissions_for(guild.me).send_messages
               and type(x) is TextChannel),
-             next((x for x in guild.channels if x.permissions_for(guild.me).send_messages and type(x) is TextChannel), None))
+             next((x for x in guild.channels if x.permissions_for(guild.me).send_messages and type(x) is TextChannel),
+                  None))
     if c is not None:
         log.info("Printing welcome message to guild {0.name} (ID: {0.id})".format(guild))
-        await c.send("Hi! I'm Starlight Glimmer. "
-                     "For a full list of commands, pull up my help page with `{}help`. "
-                     "Happy pixel painting!".format("g!"))
+        await c.send("Hi! I'm {0}. For a full list of commands, pull up my help page with `{1}help`. "
+                     "Happy pixel painting!".format(cfg.name, cfg.prefix))
     else:
         log.info("Welcome message not printed for channel {0.name} (ID: {0.id}): Could not find a default channel."
                  .format(guild))
@@ -134,11 +130,11 @@ async def on_guild_update(before, after):
 @bot.event
 async def on_command_error(ctx, error):
     if isinstance(error, commands.CommandNotFound):
-        await ctx.send("That is not a valid command. Use {}help to see my commands. Or if you're trying to use another "
-                       "bot, change my command prefix.".format(get_prefix(bot, ctx.message)))
+        await ctx.send(getlang(ctx.guild.id, "bot.error.command_not_found").format(get_prefix(bot, ctx.message)))
         return
     if isinstance(error, commands.MissingRequiredArgument):
         pages = bot.formatter.format_help_for(ctx, ctx.command)
+        print(pages)
         for p in pages:
             await ctx.send(p)
         return
@@ -148,16 +144,18 @@ async def on_command_error(ctx, error):
             await ctx.send(p)
         return
     if isinstance(error, NoPermission):
-        await ctx.send("You do not have permission to use this command.")
+        await ctx.send(getlang(ctx.guild.id, "bot.error.no_permission"))
         return
     if isinstance(error, commands.NoPrivateMessage):
-        await ctx.send("That command only works in guilds.")
+        await ctx.send(getlang(ctx.guild.id, "bot.error.no_private_message"))
         return
     cname = ctx.command.qualified_name if ctx.command is not None else "None"
     await channel_logger.log_to_channel("An error occurred while executing command `{0}` in server **{1.name}** "
                                         "(ID: `{1.id}`):".format(cname, ctx.guild))
     await channel_logger.log_to_channel("```{}```".format(error))
-    log.error("An error occurred while executing command {}: {}".format(cname, error))
+    log.error("An error occurred while executing command {}: {}\n{}"
+              .format(cname, error, ''.join(traceback.format_exception(None, error, error.__traceback__))))
+    await ctx.send(getlang(ctx.guild.id, "bot.error.unhandled_command_error"))
 
 
 @bot.event
@@ -173,27 +171,20 @@ async def on_message(message):
     if message.guild is not None and not message.channel.permissions_for(message.guild.me).send_messages:
         return
 
-    if message.content == "{} help".format(bot.user.mention):
-        pages = bot.formatter.format_help_for(ctx, bot)
-        for p in pages:
-            await ctx.send(p)
-        return
-
     if sql.select_guild_by_id(ctx.guild.id)['autoscan'] == 1:
         default_canvas = sql.select_guild_by_id(ctx.guild.id)['default_canvas']
-        pc_match = re.search(
-            '(?:(experimental\.)?pixelcanvas\.io/)@(-?\d+), ?(-?\d+)/?(?:\s?#?(\d+))?(?: ?(-e))?',
-            message.content)
-        pzio_match = re.search('(?:pixelz.io/)@(-?\d+), ?(-?\d+)/?(?:\s?#?(\d+))?', message.content)
+        pc_match = re.search('(?:pixelcanvas\.io/)@(-?\d+),(-?\d+)/?(?:\s?#?(\d+))?', message.content)
+        pzio_match = re.search('(?:pixelz\.io/)@(-?\d+),(-?\d+)(?:\s?#?(\d+))?', message.content)
+        pzone_match = re.search('(?:pixelzone\.io/)\?p=(-?\d+),(-?\d+)(?:,(\d+))?(?:\s?#?(\d+))?', message.content)
+        prev_match = re.search('@\(?(-?\d+), ?(-?\d+)\)?(?: ?#(\d+))?', message.content)
+        diff_match = re.search('\(?(-?\d+), ?(-?\d+)\)?(?: ?#(\d+))?', message.content)
 
-        prev_match = re.search('@\(?(-?\d+), ?(-?\d+)\)?(?: ?#(\d+))?(?: (-e)?)?', message.content)
-        else_match = re.search('\(?(-?\d+), ?(-?\d+)\)?(?: ?#(\d+))?(?: (-e)?)?', message.content)
         if pc_match is not None:
-            x = int(pc_match.group(2))
-            y = int(pc_match.group(3))
-            zoom = int(pc_match.group(4)) if pc_match.group(4) is not None else 1
-            is_exp = pc_match.group(1) is not None or pc_match.group(5) is not None
-            await pixelcanvasio_preview(ctx, x, y, zoom, is_exp)
+            x = int(pc_match.group(1))
+            y = int(pc_match.group(2))
+            zoom = int(pc_match.group(3)) if pc_match.group(3) is not None else 1
+            zoom = max(min(zoom, 16), 1)
+            await render.preview(ctx, x, y, zoom, render.fetch_pixelcanvas)
             return
 
         if pzio_match is not None:
@@ -201,7 +192,20 @@ async def on_message(message):
             y = int(pzio_match.group(2))
             zoom = int(pzio_match.group(3)) if pzio_match.group(3) is not None else 1
             zoom = max(min(zoom, 16), 1)
-            await pixelzio_preview(ctx, x, y, zoom)
+            await render.preview(ctx, x, y, zoom, render.fetch_pixelzio)
+            return
+
+        if pzone_match is not None:
+            x = int(pzone_match.group(1))
+            y = int(pzone_match.group(2))
+            if pzone_match.group(4) is not None:
+                zoom = int(pzone_match.group(4))
+            elif pzone_match.group(3) is not None:
+                zoom = int(pzio_match.group(3))
+            else:
+                zoom = 1
+            zoom = max(min(zoom, 16), 1)
+            await render.SIOConn().preview(ctx, x, y, zoom)
             return
 
         if prev_match is not None:
@@ -209,61 +213,82 @@ async def on_message(message):
             y = int(prev_match.group(2))
             zoom = int(prev_match.group(3)) if prev_match.group(3) is not None else 1
             zoom = max(min(zoom, 16), 1)
-            is_exp = prev_match.group(4) is not None
             if default_canvas == "pixelcanvas.io":
-                await pixelcanvasio_preview(ctx, x, y, zoom, is_exp)
+                await render.preview(ctx, x, y, zoom, render.fetch_pixelcanvas)
             elif default_canvas == "pixelz.io":
-                await pixelzio_preview(ctx, x, y, zoom)
+                await render.preview(ctx, x, y, zoom, render.fetch_pixelzio)
+            elif default_canvas == "pixelzone.io":
+                await render.SIOConn().preview(ctx, x, y, zoom)
             return
 
-        if else_match is not None:
-            x = int(else_match.group(1))
-            y = int(else_match.group(2))
-            zoom = int(else_match.group(3)) if else_match.group(3) is not None else 1
-            zoom = max(min(zoom, 16), 1)
-            is_exp = else_match.group(4) is not None
-            if len(message.attachments) > 0:
-                att = message.attachments[0]
-                if default_canvas == "pixelcanvas.io":
-                    await pixelcanvasio_diff(ctx, x, y, att, is_exp)
-                elif default_canvas == "pixelz.io":
-                    await pixelzio_diff(ctx, x, y, att)
+        if diff_match is not None and len(message.attachments) > 0 \
+                and message.attachments[0].filename[-4:].lower() == ".png":
+            att = message.attachments[0]
+            x = int(diff_match.group(1))
+            y = int(diff_match.group(2))
+            zoom = int(diff_match.group(3)) if diff_match.group(3) is not None else 1
+            zoom = max(1, min(zoom, 400 // att.width, 400 // att.height))
+            if default_canvas == "pixelcanvas.io":
+                await render.diff(ctx, x, y, att, zoom, render.fetch_pixelcanvas, pc_colors)
+            elif default_canvas == "pixelz.io":
+                await render.diff(ctx, x, y, att, zoom, render.fetch_pixelzio, pzio_colors)
+            elif default_canvas == "pixelzone.io":
+                await render.SIOConn().diff(ctx, x, y, att, zoom)
+            return
 
 
 @bot.command()
 async def ping(ctx):
-    """Pong!"""
     ping_start = time()
-    ping_msg = await ctx.send("Pinging...")
+    ping_msg = await ctx.send(getlang(ctx.guild.id, "bot.ping"))
     ping_time = time() - ping_start
-    await ping_msg.edit(content="Pong! | **{0:.01f}s**".format(ping_time))
+    await ping_msg.edit(content=getlang(ctx.guild.id, "bot.pong").format(ping_time))
 
 
 @bot.command()
 async def github(ctx):
-    """Get a link to my GitHub repository"""
     await ctx.send("https://github.com/DiamondIceNS/StarlightGlimmer")
 
 
 @bot.command()
 async def changelog(ctx):
-    """Get a link to my releases page for changelog info"""
     await ctx.send("https://github.com/DiamondIceNS/StarlightGlimmer/releases")
 
 
 @bot.command()
 async def version(ctx):
-    """Get my version"""
-    await ctx.send("My version number is **{}**".format(VERSION))
+    await ctx.send(getlang(ctx.guild.id, "bot.version").format(VERSION))
 
 
 @bot.command()
 async def suggest(ctx, *, suggestion: str):
-    """Suggest a bot feature or change to the dev"""
     await channel_logger.log_to_channel("New suggestion from **{0.name}#{0.discriminator}** (ID: `{0.id}`) in guild "
                                         "**{1.name}** (ID: `{1.id}`):".format(ctx.author, ctx.guild))
     await channel_logger.log_to_channel("> `{}`".format(suggestion))
-    await ctx.send("Your suggestion has been sent. Thank you for your input!")
+    await ctx.send(getlang(ctx.guild.id, "bot.suggest"))
+
+
+@bot.group(name="ditherchart")
+async def ditherchart():
+    pass
+
+
+@ditherchart.command(name="pixelcanvas")
+async def ditherchart_pixelcanvas(ctx):
+    f = discord.File("assets/dither_chart_pixelcanvas.png", "assets/dither_chart_pixelcanvas.png")
+    await ctx.send(file=f)
+
+
+@ditherchart.command(name="pixelzio")
+async def ditherchart_pixelzio(ctx):
+    f = discord.File("assets/dither_chart_pixelzio.png", "assets/dither_chart_pixelzio.png")
+    await ctx.send(file=f)
+
+
+@ditherchart.command(name="pixelzone")
+async def ditherchart_pixelzio(ctx):
+    f = discord.File("assets/dither_chart_pixelzone.png", "assets/dither_chart_pixelzone.png")
+    await ctx.send(file=f)
 
 
 bot.run(cfg.token)
diff --git a/lang/en_US.py b/lang/en_US.py
new file mode 100644
index 0000000..fba7825
--- /dev/null
+++ b/lang/en_US.py
@@ -0,0 +1,241 @@
+STRINGS = {
+    # General messages
+    "bot.alert_update": "I have updated to version **{0}**! Check out the command help page for new commands with `{1}help`, or visit https://github.com/DiamondIceNS/StarlightGlimmer/releases for the full changelog.",
+    "bot.description":
+        """Hi! I'm {0}! I'm here to help coordinate pixel art on pixel-placing websites.
+        I've got features like canvas preview and template checking that are sure to be helpful.
+        Let's get pixel painting!""",
+    "bot.help_ending_note": "Type '{0}{1} <command>' for more info on a command.",
+    "bot.ping": "Pinging...",
+    "bot.pong": "Pong! | **{0:.01f}s**",
+    "bot.suggest": "Your suggestion has been sent. Thank you for your input!",
+    "bot.version": "My version number is **{0}**",
+
+    # Animotes messages
+    "animotes.guild_opt_in": "Emoji sharing has been **enabled** for this guild.",
+    "animotes.guild_opt_out": "Emoji sharing has been **disabled** for this guild.",
+    "animotes.member_opt_in": "You have successfully **opted-in** to emoji conversion.",
+    "animotes.member_opt_out": "You have successfully **opted-out** of emoji conversion.",
+
+    # Canvas messages
+    "render.diff": "{0}/{1} | {2} errors | {3:.2f}% complete",
+    "render.diff_bad_color": "{0}/{1} | {2} errors | {3} bad color | {4:.2f}% complete",
+    "render.large_template": "(Processing large template, this might take a few seconds...)",
+    "render.quantize": "Fixed {0} pixels.",
+    "render.repeat_not_found": "Could not find a valid command to repeat.",
+
+    # Configuration messages
+    "configuration.alert_channel_cleared": "Alert channel has been cleared.",
+    "configuration.alert_channel_set": "Alert channel has been set to {0}.",
+    "configuration.autoscan_disabled": "Autoscan has been disabled.",
+    "configuration.autoscan_enabled": "Autoscan has been enabled.",
+    "configuration.default_canvas_set": "Default canvas has been set to **{0}**.",
+    "configuration.prefix_set": "Prefix for this guild has been set to **{0}**.",
+
+    # Error messages
+    "bot.error.command_not_found": "That is not a valid command. Use {0}help to see my commands.",
+    "bot.error.missing_attachment": "That command requires an attachment.",
+    "bot.error.no_canvas": "That command requires a subcommand.",
+    "bot.error.no_permission": "You do not have permission to use that command.",
+    "bot.error.no_png": "That command requires a PNG image.",
+    "bot.error.jpeg": "Seriously? A JPEG? Gross! Please create a PNG template instead.",
+    "bot.error.no_private_message": "That command only works in guilds.",
+    "bot.error.unhandled_command_error": "An error occurred with that command. The dev has been notified.",
+
+    # Command brief help
+    "brief.alertchannel": "Set or clear the channel used for update alerts.",
+    "brief.alertchannel.clear": "Clears the alert channel.",
+    "brief.alertchannel.set": "Sets the alert channel.",
+    "brief.autoscan": "Toggles automatic preview and diff.",
+    "brief.changelog": "Gets a link to my releases page.",
+    "brief.diff": "Checks completion status of a template on a canvas.",
+    "brief.diff.pixelcanvas": "Creates a diff using Pixelcanvas.io.",
+    "brief.diff.pixelzio": "Creates a diff using Pixelx.io.",
+    "brief.diff.pixelzone": "Creates a diff using Pixelzone.io.",
+    "brief.ditherchart": "Gets a chart of canvas colors dithered together.",
+    "brief.ditherchart.pixelcanvas": "Gets a dither chart of Pixelcanvas colors.",
+    "brief.ditherchart.pixelzio": "Gets a dither chart of Pixelz.io colors.",
+    "brief.ditherchart.pixelzone": "Gets a dither chart of Pixelzone colors",
+    "brief.github": "Gets a link to my GitHub repository.",
+    "brief.help": "Displays this message.",
+    "brief.listemotes": "Lists all the animated emoji that I know about.",
+    "brief.ping": "Pong!",
+    "brief.preview": "Previews the canvas at a given coordinate.",
+    "brief.preview.pixelcanvas": "Creates a preview using Pixelcanvas.io.",
+    "brief.preview.pixelzio": "Creates a preview using Pixelz.io.",
+    "brief.preview.pixelzone": "Creates a preview using Pixelzone.io.",
+    "brief.quantize": "Rough converts an image to the palette of a canvas.",
+    "brief.quantize.pixelcanvas": "Quantizes colors using the palette of Pixelcanvas.io.",
+    "brief.quantize.pixelzio": "Quantizes colors using the palette of Pixelz.io.",
+    "brief.quantize.pixelzone": "Quantizes colors using the palette of Pixelzone.io.",
+    "brief.register": "Toggles animated emoji replacement for a user.",
+    "brief.registerguild": "Toggles emoji sharing for this guild.",
+    "brief.repeat": "Repeats the last used canvas command.",
+    "brief.setdefaultcanvas": "Sets the default canvas website for this guild.",
+    "brief.setdefaultcanvas.pixelcanvas": "Sets the default canvas to Pixelcanvas.io.",
+    "brief.setdefaultcanvas.pixelzio": "Sets the default canvas to Pixelz.io.",
+    "brief.setdefaultcanvas.pixelzone": "Sets the default canvas to Pixelzone.io.",
+    "brief.setprefix": "Sets my command prefix for this guild.",
+    "brief.suggest": "Sends a suggestion to the developer.",
+    "brief.version": "Gets my version number.",
+
+    # Command long help
+    "help.alertchannel": """If an alert channel is set, I will post a message in that channel any time my version number changes to alert you to updates.""",
+    "help.alertchannel.clear": """This effectively disables update alerts until a new channel is set.""",
+    "help.alertchannel.set":
+        """Use the #channel mention syntax with this command to ensure the correct channel is set.
+    
+        This command can only be used by members with the Administrator permission.""",
+    "help.autoscan":
+        """If enabled, I will watch all messages for coordinates and automatically create previews and diffs according to these rules:
+        - Any message with coordinates in the form "@0, 0" will trigger a preview for the default canvas.
+        - Any message with a link to a supported canvas will trigger a preview for that canvas.
+        - Any message with coordinates in the form "0, 0" with a PNG attached will trigger a diff for the default canvas.
+        - Previews take precedence over diffs
+        
+        See 'setdefaultcanvas' for more information about the default canvas.
+        
+        Only users with the Administrator role can use this command.""",
+    "help.changelog": None,
+    "help.diff":
+        """Takes an uploaded template, compares it to the current state of the canvas, and calculates how complete it is. It will also generate an image showing you where the unfinished pixels are.
+        
+        If the template is smaller than 200x200, you can create a larger image with a zoom factor. (i.e. "0, 0 #4) You cannot zoom an image to be larger than 400x400.
+        
+        Template must be PNG format.
+        
+        NOTE: "Bad color" pixels are pixels that are not part of the canvas's palette. (See `quantize`)
+        
+        If autoscan is enabled, this happens automatically using the default canvas. (See 'autoscan' and 'setdefaultcanvas')""",
+    "help.diff.pixelcanvas": None,
+    "help.diff.pixelzio": None,
+    "help.diff.pixelzone": None,
+    "help.ditherchart": None,
+    "help.ditherchart.pixelcanvas": None,
+    "help.ditherchart.pixelzio": None,
+    "help.ditherchart.pixelzone": None,
+    "help.github": None,
+    "help.help": None,
+    "help.listemotes": """See 'registerserver' for more information about emoji sharing.""",
+    "help.ping": None,
+    "help.preview":
+        """Given a coordinate pair or a URL, renders a live view of a canvas at those coordinates.
+        
+        You can create a zoomed-in preview by adding a zoom factor. (i.e. "0, 0 #4") Maximum zoom is 16.
+        
+        If autoscan is enabled, this happens automatically using the default canvas. (See 'autoscan' and 'setdefaultcanvas')""",
+    "help.preview.pixelcanvas": None,
+    "help.preview.pixelzio": None,
+    "help.preview.pixelzone": None,
+    "help.quantize":
+        """Takes an attached image and converts its colors to the palette of a given canvas.
+        
+        This should primarily be used if the 'pcdiff' command is telling you your template has 'bad color' in it. Using this command to create templates from raw images is not suggested.""",
+    "help.quantize.pixelcanvas": None,
+    "help.quantize.pixelzio": None,
+    "help.quantize.pixelzone": None,
+    "help.register":
+        """If you opt-in with this command, I will watch for any time you try to use an animated emoji and replace your message with another that has the emoji in it. You only need to opt-in once for this to apply to all guilds. Use this command again to opt-out.
+        
+        If your guild has opted-in to emoji sharing, you can use emoji from any other guild that has also opted-in. (See 'registerguild')
+        
+        I can't use animated emoji from guilds I am not in, so I cannot use animated emoji from other guilds posted by Discord Nitro users or from Twitch-integrated guilds.
+        
+        This feature requires that I have the Manage Messages permission.""",
+    "help.registerguild":
+        """If opted-in, members of this guild will be able to use animated emoji from any other guild that has also opted-in. In return, animated emoji from this guild can also be used by any of those guilds. This is not required to use animated emoji from this guild.
+        
+        NOTE: Opting-in to emoji sharing will let other guilds see this guild's name and ID. If your guild is not a public guild, enabling this feature is not recommended.
+        
+        This command can only be used by members with the Manage Emojis permission.""",
+    "help.repeat": "This command only applies to 'preview', 'diff', and their autoscan invocations. Only 50 messages back will be searched.",
+    "help.setdefaultcanvas":
+        """The default canvas is the canvas that will be used for automatic previews or diffs triggered by autoscan. (See 'autoscan')
+        
+        Defaults to Pixelcanvas.io.
+        
+        This command can only be used by members with the Administrator permission.""",
+    "help.setdefaultcanvas.pixelcanvas": """This command can only be used by members with the Administrator permission.""",
+    "help.setdefaultcanvas.pixelzio": """This command can only be used by members with the Administrator permission.""",
+    "help.setdefaultcanvas.pixelzone": """This command can only be used by members with the Administrator permission.""",
+    "help.setprefix":
+        """Max length is 10 characters. You really shouldn't need more than 2.
+        
+        This command can only be used by members with the Administrator permission.""",
+    "help.suggest": None,
+    "help.version": None,
+
+    # Command names
+    "command.alertchannel": "alertchannel",
+    "command.alertchannel.clear": "clear",
+    "command.alertchannel.set": "set",
+    "command.autoscan": "autoscan",
+    "command.changelog": "changelog",
+    "command.diff": "diff",
+    "command.diff.pixelcanvas": "pixelcanvas",
+    "command.diff.pixelzio": "pixelzio",
+    "command.diff.pixelzone": "pixelzone",
+    "command.ditherchart": "ditherchart",
+    "command.ditherchart.pixelcanvas": "pixelcanvas",
+    "command.ditherchart.pixelzio": "pixelzio",
+    "command.ditherchart.pixelzone": "pixelzone",
+    "command.github": "github",
+    "command.help": "help",
+    "command.listemotes": "listemotes",
+    "command.ping": "ping",
+    "command.preview": "preview",
+    "command.preview.pixelcanvas": "pixelcanvas",
+    "command.preview.pixelzio": "pixelzio",
+    "command.preview.pixelzone": "pixelzone",
+    "command.quantize": "quantize",
+    "command.quantize.pixelcanvas": "pixelcanvas",
+    "command.quantize.pixelzio": "pixelzio",
+    "command.quantize.pixelzone": "pixelzone",
+    "command.register": "register",
+    "command.registerguild": "registerguild",
+    "command.repeat": "repeat",
+    "command.setdefaultcanvas": "setdefaultcanvas",
+    "command.setdefaultcanvas.pixelcanvas": "pixelcanvas",
+    "command.setdefaultcanvas.pixelzio": "pixelzio",
+    "command.setdefaultcanvas.pixelzone": "pixelzone",
+    "command.setprefix": "setprefix",
+    "command.suggest": "suggest",
+    "command.version": "version",
+
+    # Command signatures
+    "signature.alertchannel": "alertchannel <subcommand>",
+    "signature.alertchannel.clear": "alertchannel clear",
+    "signature.alertchannel.set": "alertchannel set <channel>",
+    "signature.autoscan": "autoscan",
+    "signature.changelog": "changelog",
+    "signature.diff": "diff <subcommand>",
+    "signature.diff.pixelcanvas": "diff pixelcanvas <coordinates> (zoom)",
+    "signature.diff.pixelzio": "diff pixelzio <coordinates> (zoom)",
+    "signature.diff.pixelzone": "diff pixelzone <coordinates> (zoom)",
+    "signature.ditherchart": "ditherchart <subcommand>",
+    "signature.ditherchart.pixelcanvas": "ditherchart pixelcanvas",
+    "signature.ditherchart.pixelzio": "ditherchart pixelzio",
+    "signature.ditherchart.pixelzone": "ditherchart pixelzone",
+    "signature.github": "github",
+    "signature.help": "help",
+    "signature.listemotes": "listemotes",
+    "signature.ping": "ping",
+    "signature.preview": "preview <subcommand>",
+    "signature.preview.pixelcanvas": "preview pixelcanvas <coordinates> (zoom)",
+    "signature.preview.pixelzio": "preview pixelzio <coordinates> (zoom)",
+    "signature.preview.pixelzone": "preview pixelzone <coordinates> (zoom)",
+    "signature.quantize": "quantize <subcommand>",
+    "signature.quantize.pixelcanvas": "quantize pixelcanvas",
+    "signature.quantize.pixelzio": "quantize pixelzio",
+    "signature.quantize.pixelzone": "quantize pixelzone",
+    "signature.register": "register",
+    "signature.registerguild": "registerguild",
+    "signature.repeat": "repeat",
+    "signature.setdefaultcanvas": "setdefaultcanvas <subcommand>",
+    "signature.setdefaultcanvas.pixelcanvas": "setdefaultcanvas pixelcanvas",
+    "signature.setdefaultcanvas.pixelzio": "setdefaultcanvas pixelzio",
+    "signature.setdefaultcanvas.pixelzone": "setdefaultcanvas pixelzone",
+    "signature.setprefix": "setprefix <prefix>",
+    "signature.suggest": "suggest <suggestion>",
+    "signature.version": "version",
+}
diff --git a/CC0.txt b/licenses/CC0.txt
similarity index 100%
rename from CC0.txt
rename to licenses/CC0.txt
diff --git a/GPL3.txt b/licenses/GPL3.txt
similarity index 100%
rename from GPL3.txt
rename to licenses/GPL3.txt
diff --git a/licenses/WTFPL2.txt b/licenses/WTFPL2.txt
new file mode 100644
index 0000000..4a87280
--- /dev/null
+++ b/licenses/WTFPL2.txt
@@ -0,0 +1,13 @@
+        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+                    Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. You just DO WHAT THE FUCK YOU WANT TO.
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 2a3ad4d..e09fbb5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,5 @@
 git+https://github.com/Rapptz/discord.py@rewrite
 pillow
-aiohttp
\ No newline at end of file
+aiohttp
+socketio_client
+lz4
\ No newline at end of file
diff --git a/utils/constants.py b/utils/colors.py
similarity index 65%
rename from utils/constants.py
rename to utils/colors.py
index 810b0e4..9bc8e12 100644
--- a/utils/constants.py
+++ b/utils/colors.py
@@ -1,5 +1,3 @@
-pc_url_main = 'http://pixelcanvas.io/api/bigchunk/'
-pc_url_exp = 'http://experimental.pixelcanvas.io/api/bigchunk/'
 pc_colors = [
     (255, 255, 255),  # White
     (228, 228, 228),  # Light Grey
@@ -19,8 +17,7 @@
     (130,   0, 128)   # Purple
 ]
 
-pixelz_url = 'http://pixelz.io/api/'
-pixelz_colors = [
+pzio_colors = [
     (255, 255, 255),  # White
     (228, 228, 228),  # Light Grey
     (136, 136, 136),  # Dark Grey
@@ -38,3 +35,22 @@
     (209,  89, 230),  # Light Purple
     (130,   0, 128)   # Purple
 ]
+
+pzone_colors = [
+    ( 38,  38,	38),  # Dark Grey
+    (  0,	0,	 0),  # Black
+    (128, 128, 128),  # Light Grey
+    (255, 255, 255),  # White
+    (153,  98,  61),  # Brown
+    (255, 163, 200),  # Pink
+    (207, 115, 230),  # Light Purple
+    (128,   0, 128),  # Purple
+    (229,   0,   0),  # Red
+    (229, 137,   0),  # Orange
+    (229, 229,   0),  # Yellow
+    (150, 230,  70),  # Light Green
+    (  0, 190,   0),  # Green
+    (  0, 230, 230),  # Cyan
+    (  0, 136, 207),  # Teal
+    (  0,   0, 230)   # Blue
+]
diff --git a/utils/config.py b/utils/config.py
index a4c059e..c35ef86 100644
--- a/utils/config.py
+++ b/utils/config.py
@@ -27,6 +27,7 @@ def __init__(self):
 
         self.token = data.get('token', None)
         self.prefix = data.get('prefix', "g!")
+        self.name = data.get('name', 'Starlight Glimmer')
 
         self.preview_h = clamp(data.get('preview_height', 240), 0, 896)
         self.preview_w = clamp(data.get('preview_width', 400), 0, 896)
diff --git a/utils/help_formatter.py b/utils/help_formatter.py
new file mode 100644
index 0000000..c181f66
--- /dev/null
+++ b/utils/help_formatter.py
@@ -0,0 +1,88 @@
+import asyncio
+import itertools
+import inspect
+from discord.ext.commands import Command
+from discord.ext.commands.formatter import HelpFormatter, Paginator
+
+from utils.config import Config
+from utils.language import getlang
+
+cfg = Config()
+
+
+class GlimmerHelpFormatter(HelpFormatter):
+    def get_localized_ending_note(self):
+        gid = self.context.guild.id
+        command_name = self.context.invoked_with
+        return getlang(gid, "bot.help_ending_note").format(self.clean_prefix, command_name)
+
+    def add_localized_subcommands_to_page(self, max_width, commands):
+        gid = self.context.guild.id
+        for name, command in commands:
+            if name in command.aliases:
+                # skip aliases
+                continue
+
+            short_doc = getlang(gid, "brief."+command.qualified_name.replace(' ', '.'))
+            entry = '  {0:<{width}} - {1}'.format(name, short_doc, width=max_width)
+            shortened = self.shorten(entry)
+            self._paginator.add_line(shortened)
+
+    @asyncio.coroutine
+    def format(self):
+        self._paginator = Paginator()
+        gid = self.context.guild.id
+
+        if self.is_bot():
+            self._paginator.add_line(inspect.cleandoc(getlang(gid, "bot.description").format(cfg.name)), empty=True)
+        elif self.is_cog():
+            # self._paginator.add_line(getlang(gid, ))
+            pass  # TODO: HELP!!
+        elif isinstance(self.command, Command):
+            self._paginator.add_line(getlang(gid, "brief."+self.command.qualified_name.replace(' ', '.')), empty=True)
+
+            # TODO: Translate signatures
+            # <signature portion>
+            signature = self.get_command_signature()
+            self._paginator.add_line(signature, empty=True)
+
+            # <long doc> section
+            long_doc = getlang(gid, "help." + self.command.qualified_name.replace(' ', '.'))
+            if long_doc:
+                self._paginator.add_line(inspect.cleandoc(long_doc), empty=True)
+
+            # end it here if it's just a regular command
+            if not self.has_subcommands():
+                self._paginator.close_page()
+                return self._paginator.pages
+
+        max_width = self.max_name_size
+
+        def category(tup):
+            cog = tup[1].cog_name
+            # we insert the zero width space there to give it approximate
+            # last place sorting position.
+            return cog + ':' if cog is not None else '\u200bNo Category:'
+
+        filtered = yield from self.filter_command_list()
+        if self.is_bot():
+            data = sorted(filtered, key=category)
+            for category, commands in itertools.groupby(data, key=category):
+                # there simply is no prettier way of doing this.
+                commands = sorted(commands)
+                if len(commands) > 0:
+                    self._paginator.add_line(category)
+
+                self.add_localized_subcommands_to_page(max_width, commands)
+        else:
+            filtered = sorted(filtered)
+            if filtered:
+                self._paginator.add_line('Commands:')
+                self.add_localized_subcommands_to_page(max_width, filtered)
+
+        # add the ending note
+        self._paginator.add_line()
+        ending_note = self.get_localized_ending_note()
+        self._paginator.add_line(ending_note)
+
+        return self._paginator.pages
diff --git a/utils/language.py b/utils/language.py
new file mode 100644
index 0000000..2f2744e
--- /dev/null
+++ b/utils/language.py
@@ -0,0 +1,11 @@
+import utils.sqlite
+import lang.en_US
+
+
+def getlang(guild_id, str_id):
+    language = utils.sqlite.get_guild_language(guild_id)
+
+    if language == "en_US":
+        return lang.en_US.STRINGS[str_id]
+
+
diff --git a/utils/logger.py b/utils/logger.py
index e4a0cb9..8b323c9 100644
--- a/utils/logger.py
+++ b/utils/logger.py
@@ -8,7 +8,7 @@ class Log:
     def __init__(self, name):
         self.logger = logging.getLogger(name)
         self.logger.setLevel(logging.DEBUG)
-        handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='a')
+        handler = logging.FileHandler(filename='data/discord.log', encoding='utf-8', mode='a')
         handler.setFormatter(logging.Formatter('{asctime} [{levelname}] {name}: {message}', style='{'))
         self.logger.addHandler(handler)
 
diff --git a/utils/lzstring.py b/utils/lzstring.py
new file mode 100644
index 0000000..749dfc4
--- /dev/null
+++ b/utils/lzstring.py
@@ -0,0 +1,425 @@
+"""
+Copyright © 2017 Marcel Dancak <dancakm@gmail.com>
+This work is free. You can redistribute it and/or modify it under the
+terms of the Do What The Fuck You Want To Public License, Version 2,
+as published by Sam Hocevar. See the COPYING file for more details.
+"""
+
+import math
+
+
+keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
+keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"
+baseReverseDic = {};
+
+class Object(object):
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+
+def getBaseValue(alphabet, character):
+    if alphabet not in baseReverseDic:
+        baseReverseDic[alphabet] = {}
+    for i in range(len(alphabet)):
+        baseReverseDic[alphabet][alphabet[i]] = i
+    return baseReverseDic[alphabet][character]
+
+
+def _compress(uncompressed, bitsPerChar, getCharFromInt):
+    if (uncompressed is None):
+        return ""
+
+    context_dictionary = {}
+    context_dictionaryToCreate= {}
+    context_c = ""
+    context_wc = ""
+    context_w = ""
+    context_enlargeIn = 2 # Compensate for the first entry which should not count
+    context_dictSize = 3
+    context_numBits = 2
+    context_data = []
+    context_data_val = 0
+    context_data_position = 0
+
+    for ii in range(len(uncompressed)):
+        context_c = uncompressed[ii]
+        if context_c not in context_dictionary:
+            context_dictionary[context_c] = context_dictSize
+            context_dictSize += 1
+            context_dictionaryToCreate[context_c] = True
+
+        context_wc = context_w + context_c
+        if context_wc in context_dictionary:
+            context_w = context_wc
+        else:
+            if context_w in context_dictionaryToCreate:
+                if ord(context_w[0]) < 256:
+                    for i in range(context_numBits):
+                        context_data_val = (context_data_val << 1)
+                        if context_data_position == bitsPerChar-1:
+                            context_data_position = 0
+                            context_data.append(getCharFromInt(context_data_val))
+                            context_data_val = 0
+                        else:
+                            context_data_position += 1
+                    value = ord(context_w[0])
+                    for i in range(8):
+                        context_data_val = (context_data_val << 1) | (value & 1)
+                        if context_data_position == bitsPerChar - 1:
+                            context_data_position = 0
+                            context_data.append(getCharFromInt(context_data_val))
+                            context_data_val = 0
+                        else:
+                            context_data_position += 1
+                        value = value >> 1
+
+                else:
+                    value = 1
+                    for i in range(context_numBits):
+                        context_data_val = (context_data_val << 1) | value
+                        if context_data_position == bitsPerChar - 1:
+                            context_data_position = 0
+                            context_data.append(getCharFromInt(context_data_val))
+                            context_data_val = 0
+                        else:
+                            context_data_position += 1
+                        value = 0
+                    value = ord(context_w[0])
+                    for i in range(16):
+                        context_data_val = (context_data_val << 1) | (value & 1)
+                        if context_data_position == bitsPerChar - 1:
+                            context_data_position = 0
+                            context_data.append(getCharFromInt(context_data_val))
+                            context_data_val = 0
+                        else:
+                            context_data_position += 1
+                        value = value >> 1
+                context_enlargeIn -= 1
+                if context_enlargeIn == 0:
+                    context_enlargeIn = math.pow(2, context_numBits)
+                    context_numBits += 1
+                del context_dictionaryToCreate[context_w]
+            else:
+                value = context_dictionary[context_w]
+                for i in range(context_numBits):
+                    context_data_val = (context_data_val << 1) | (value & 1)
+                    if context_data_position == bitsPerChar - 1:
+                        context_data_position = 0
+                        context_data.append(getCharFromInt(context_data_val))
+                        context_data_val = 0
+                    else:
+                        context_data_position += 1
+                    value = value >> 1
+
+            context_enlargeIn -= 1
+            if context_enlargeIn == 0:
+                context_enlargeIn = math.pow(2, context_numBits)
+                context_numBits += 1
+            
+            # Add wc to the dictionary.
+            context_dictionary[context_wc] = context_dictSize
+            context_dictSize += 1
+            context_w = str(context_c)
+
+    # Output the code for w.
+    if context_w != "":
+        if context_w in context_dictionaryToCreate:
+            if ord(context_w[0]) < 256:
+                for i in range(context_numBits):
+                    context_data_val = (context_data_val << 1)
+                    if context_data_position == bitsPerChar-1:
+                        context_data_position = 0
+                        context_data.append(getCharFromInt(context_data_val))
+                        context_data_val = 0
+                    else:
+                        context_data_position += 1
+                value = ord(context_w[0])
+                for i in range(8):
+                    context_data_val = (context_data_val << 1) | (value & 1)
+                    if context_data_position == bitsPerChar - 1:
+                        context_data_position = 0
+                        context_data.append(getCharFromInt(context_data_val))
+                        context_data_val = 0
+                    else:
+                        context_data_position += 1
+                    value = value >> 1
+            else:
+                value = 1
+                for i in range(context_numBits):
+                    context_data_val = (context_data_val << 1) | value
+                    if context_data_position == bitsPerChar - 1:
+                        context_data_position = 0
+                        context_data.append(getCharFromInt(context_data_val))
+                        context_data_val = 0
+                    else:
+                        context_data_position += 1
+                    value = 0
+                value = ord(context_w[0])
+                for i in range(16):
+                    context_data_val = (context_data_val << 1) | (value & 1)
+                    if context_data_position == bitsPerChar - 1:
+                        context_data_position = 0
+                        context_data.append(getCharFromInt(context_data_val))
+                        context_data_val = 0
+                    else:
+                        context_data_position += 1
+                    value = value >> 1
+            context_enlargeIn -= 1
+            if context_enlargeIn == 0:
+                context_enlargeIn = math.pow(2, context_numBits)
+                context_numBits += 1
+            del context_dictionaryToCreate[context_w]
+        else:
+            value = context_dictionary[context_w]
+            for i in range(context_numBits):
+                context_data_val = (context_data_val << 1) | (value & 1)
+                if context_data_position == bitsPerChar - 1:
+                    context_data_position = 0
+                    context_data.append(getCharFromInt(context_data_val))
+                    context_data_val = 0
+                else:
+                    context_data_position += 1
+                value = value >> 1
+
+    context_enlargeIn -= 1
+    if context_enlargeIn == 0:
+        context_enlargeIn = math.pow(2, context_numBits)
+        context_numBits += 1
+
+    # Mark the end of the stream
+    value = 2
+    for i in range(context_numBits):
+        context_data_val = (context_data_val << 1) | (value & 1)
+        if context_data_position == bitsPerChar - 1:
+            context_data_position = 0
+            context_data.append(getCharFromInt(context_data_val))
+            context_data_val = 0
+        else:
+            context_data_position += 1
+        value = value >> 1
+
+    # Flush the last char
+    while True:
+        context_data_val = (context_data_val << 1)
+        if context_data_position == bitsPerChar - 1:
+            context_data.append(getCharFromInt(context_data_val))
+            break
+        else:
+           context_data_position += 1
+
+    return "".join(context_data)
+
+
+def _decompress(length, resetValue, getNextValue):
+    dictionary = {}
+    enlargeIn = 4
+    dictSize = 4
+    numBits = 3
+    entry = ""
+    result = []
+
+    data = Object(
+        val=getNextValue(0),
+        position=resetValue,
+        index=1
+    )
+
+    for i in range(3):
+        dictionary[i] = i
+
+    bits = 0
+    maxpower = math.pow(2, 2)
+    power = 1
+
+    while power != maxpower:
+        resb = data.val & data.position
+        data.position >>= 1
+        if data.position == 0:
+            data.position = resetValue
+            data.val = getNextValue(data.index)
+            data.index += 1
+
+        bits |= power if resb > 0 else 0
+        power <<= 1;
+
+    next = bits
+    if next == 0:
+        bits = 0
+        maxpower = math.pow(2, 8)
+        power = 1
+        while power != maxpower:
+            resb = data.val & data.position
+            data.position >>= 1
+            if data.position == 0:
+                data.position = resetValue
+                data.val = getNextValue(data.index)
+                data.index += 1
+            bits |= power if resb > 0 else 0
+            power <<= 1
+        c = chr(bits)
+    elif next == 1:
+        bits = 0
+        maxpower = math.pow(2, 16)
+        power = 1
+        while power != maxpower:
+            resb = data.val & data.position
+            data.position >>= 1
+            if data.position == 0:
+                data.position = resetValue;
+                data.val = getNextValue(data.index)
+                data.index += 1
+            bits |= power if resb > 0 else 0
+            power <<= 1
+        c = chr(bits)
+    elif next == 2:
+        return ""
+
+    # print(bits)
+    dictionary[3] = c
+    w = c
+    result.append(c)
+    counter = 0
+    while True:
+        counter += 1
+        if data.index > length:
+            return ""
+
+        bits = 0
+        maxpower = math.pow(2, numBits)
+        power = 1
+        while power != maxpower:
+            resb = data.val & data.position
+            data.position >>= 1
+            if data.position == 0:
+                data.position = resetValue;
+                data.val = getNextValue(data.index)
+                data.index += 1
+            bits |= power if resb > 0 else 0
+            power <<= 1
+
+        c = bits
+        if c == 0:
+            bits = 0
+            maxpower = math.pow(2, 8)
+            power = 1
+            while power != maxpower:
+                resb = data.val & data.position
+                data.position >>= 1
+                if data.position == 0:
+                    data.position = resetValue
+                    data.val = getNextValue(data.index)
+                    data.index += 1
+                bits |= power if resb > 0 else 0
+                power <<= 1
+
+            dictionary[dictSize] = chr(bits)
+            dictSize += 1
+            c = dictSize - 1
+            enlargeIn -= 1
+        elif c == 1:
+            bits = 0
+            maxpower = math.pow(2, 16)
+            power = 1
+            while power != maxpower:
+                resb = data.val & data.position
+                data.position >>= 1
+                if data.position == 0:
+                    data.position = resetValue;
+                    data.val = getNextValue(data.index)
+                    data.index += 1
+                bits |= power if resb > 0 else 0
+                power <<= 1
+            dictionary[dictSize] = chr(bits)
+            dictSize += 1
+            c = dictSize - 1
+            enlargeIn -= 1
+        elif c == 2:
+            return "".join(result)
+
+
+        if enlargeIn == 0:
+            enlargeIn = math.pow(2, numBits)
+            numBits += 1
+
+        if c in dictionary:
+            entry = dictionary[c]
+        else:
+            if c == dictSize:
+                entry = w + w[0]
+            else:
+                return None
+        result.append(entry)
+
+        # Add w+entry[0] to the dictionary.
+        dictionary[dictSize] = w + entry[0]
+        dictSize += 1
+        enlargeIn -= 1
+
+        w = entry
+        if enlargeIn == 0:
+            enlargeIn = math.pow(2, numBits)
+            numBits += 1
+
+
+class LZString(object):
+    @staticmethod
+    def compress(uncompressed):
+        return _compress(uncompressed, 16, chr)
+
+    @staticmethod
+    def compressToUTF16(uncompressed):
+        if uncompressed is None:
+            return ""
+        return _compress(uncompressed, 15, lambda a: chr(a+32)) + " "
+
+    @staticmethod
+    def compressToBase64(uncompressed):
+        if uncompressed is None:
+            return ""
+        res = _compress(uncompressed, 6, lambda a: keyStrBase64[a])
+        # To produce valid Base64
+        end = len(res) % 4
+        print (end)
+        if end > 0:
+            res += "="*(4 - end)
+        return res
+
+    @staticmethod
+    def compressToEncodedURIComponent(uncompressed):
+        if uncompressed is None:
+            return ""
+        return _compress(uncompressed, 6, lambda a: keyStrUriSafe[a])
+
+    @staticmethod
+    def decompress(compressed):
+        if compressed is None:
+            return ""
+        if compressed == "":
+            return None
+        return _decompress(len(compressed), 32768, lambda index: ord(compressed[index]))
+
+    @staticmethod
+    def decompressFromUTF16(compressed):
+        if compressed is None:
+            return ""
+        if compressed == "":
+            return None
+        return _decompress(len(compressed), 16384, lambda index: compressed[index] - 32)
+
+    @staticmethod
+    def decompressFromBase64(compressed):
+        if compressed is None:
+            return ""
+        if compressed == "":
+            return None
+        return _decompress(len(compressed), 32, lambda index: getBaseValue(keyStrBase64, compressed[index]))
+
+    @staticmethod
+    def decompressFromEncodedURIComponent(compressed):
+        if compressed is None:
+            return ""
+        if compressed == "":
+            return None
+        compressed = compressed.replace(" ", "+")
+        return _decompress(len(compressed), 32, lambda index: getBaseValue(keyStrUriSafe, compressed[index]))
diff --git a/utils/render.py b/utils/render.py
index b4c20b2..074a022 100644
--- a/utils/render.py
+++ b/utils/render.py
@@ -1,13 +1,19 @@
 import aiohttp
 import discord
-import os
 import io
+import json
+import lz4.frame
+import os
+from math import sqrt, pow
 from PIL import Image
+from socketIO_client import SocketIO
 from time import time
 
-from utils.logger import Log
 from utils.config import Config
-from utils.constants import *
+from utils.colors import *
+from utils.logger import Log
+from utils.language import getlang
+from utils.lzstring import LZString
 
 cfg = Config()
 log = Log(__name__)
@@ -20,242 +26,294 @@ def __init__(self, x, y):
         self.y = y
 
 
-async def pixelcanvasio_preview(ctx, x, y, zoom, is_experimental):
+# Pixelzone requires a unique architecture due to its use of Socket.io
+class SIOConn:
+    def __init__(self):
+        self.socket = SocketIO('http://pixelzone.io')
+        self.socket.on('chunkData', self.on_chunkdata)
+        self.chk = None
+        self.fetched = None
+        self.callbacks = 0
+
+    def fetch(self, x, y, dx, dy):
+        tlp = Coords(x + 4096, y + 4096)
+        ch_off = Coords(tlp.x % 512, tlp.y % 512)
+        ext = Coords((tlp.x + dx) // 512 - tlp.x // 512 + 1, (tlp.y + dy) // 512 - tlp.y // 512 + 1)
+        self.chk = Coords(tlp.x // 512, tlp.y // 512)
+        self.fetched = Image.new('RGB', (512 * ext.x, 512 * ext.y))
+
+        for iy in range(ext.y):
+            for ix in range(ext.x):
+                self.socket.emit('getChunkData', {"cx": self.chk.x + ix, "cy": self.chk.y + iy})
+
+        expected_callbacks = ext.x * ext.y
+        while self.callbacks < expected_callbacks:
+            self.socket.wait(seconds=1)
+        self.socket.disconnect()
+
+        self.fetched = self.fetched.crop((ch_off.x, ch_off.y, ch_off.x + dx, ch_off.y + dy))
+
+    def on_chunkdata(self, data):
+        chk_abs = Coords(data['cx'], data['cy'])
+        chk_rel = Coords(chk_abs.x - self.chk.x, chk_abs.y - self.chk.y)
+        tmp = LZString().decompressFromBase64(data['data'])
+        tmp = json.loads("[" + tmp + "]")
+        tmp = lz4.frame.decompress(bytes(tmp))
+        chunk = Image.new('RGB', (512, 512), (255, 255, 255, 255))
+        for py in range(chunk.height):
+            for px in range(chunk.width):
+                i = (py * 512 + px) / 2
+                color_id = tmp[int(i)] & 15 if i % 1 == 0 else tmp[int(i)] >> 4
+                color = pzone_colors[color_id]
+                chunk.putpixel((px, py), color)
+        self.fetched.paste(chunk, (chk_rel.x * 512, chk_rel.y * 512, (chk_rel.x + 1) * 512, (chk_rel.y + 1) * 512))
+        self.callbacks += 1
+
+    async def preview(self, ctx, x, y, zoom):
+        async with ctx.typing():
+            log.debug("X:{0} | Y:{1} | Zoom:{2}".format(x, y, zoom))
+
+            self.fetch(x - cfg.preview_w // 2, y - cfg.preview_h // 2, cfg.preview_w, cfg.preview_h)
+
+            if zoom > 1:
+                self.fetched = self.fetched.resize(tuple(zoom * x for x in self.fetched.size), Image.NEAREST)
+                tlp_z = Coords(self.fetched.width // 2 - cfg.preview_w // 2,
+                               self.fetched.height // 2 - cfg.preview_h // 2)
+                self.fetched = self.fetched.crop((tlp_z.x, tlp_z.y, tlp_z.x + cfg.preview_w, tlp_z.y + cfg.preview_h))
+
+            preview_filename = 'preview_{0}.png'.format(int(time()))
+            with open(preview_filename, 'wb') as f:
+                self.fetched.save(f, 'png')
+            f = discord.File(preview_filename, "preview.png")
+            await ctx.channel.send(file=f)
+            os.remove(preview_filename)
+
+    async def diff(self, ctx, x, y, att, zoom):
+        async with ctx.typing():
+            self.fetch(x, y, att.width, att.height)
+
+            template_filename = 'template_{0}.png'.format(int(time()))
+            with open(template_filename, 'wb') as f:
+                await att.save(f)
+            template = Image.open(template_filename).convert('RGBA')
+            os.remove(template_filename)
+
+            log.debug("X:{0} | Y:{1} | Dim: {2}x{3} | Zoom: {4}".format(x, y, template.width, template.height, zoom))
+
+            if template.width * template.height > 1000000:
+                await ctx.channel.send(getlang(ctx.guild.id, "render.large_template"))
+
+            tot = 0  # Total non-transparent pixels in template
+            err = 0  # Number of errors
+            bad = 0  # Number of pixels in the template that are not in the Pixelcanvas color palette
+
+            diff_img = Image.new('RGB', (att.width, att.height), (255, 255, 255))
+            for py in range(diff_img.height):
+                for px in range(diff_img.width):
+                    tp = template.getpixel((px, py))
+                    dp = self.fetched.getpixel((px, py))
+                    if tp[3] is not 0:
+                        # All non-transparent pixels count to the total
+                        tot += 1
+                    if 0 < tp[3] < 255 or (tp[3] is 255 and tp[:3] not in pzone_colors):
+                        # All non-opaque and non-transparent pixels, and opaque pixels of bad colors, are bad
+                        pixel = (0, 0, 255, 255)
+                        err += 1
+                        bad += 1
+                    elif tp[3] is 255 and (tp[0] is not dp[0] or tp[1] is not dp[1] or tp[2] is not dp[2]):
+                        # All pixels that are valid and opaque but do not match the canvas are wrong
+                        pixel = (255, 0, 0, 255)
+                        err += 1
+                    else:
+                        # Render all correct/irrelevant pixels in greyscale
+                        avg = round(dp[0] * 0.3 + dp[1] * 0.52 + dp[2] * 0.18)
+                        pixel = (avg, avg, avg, 255)
+
+                    diff_img.putpixel((px, py), pixel)
+
+            if zoom > 1:
+                diff_img = diff_img.resize(tuple(zoom * x for x in diff_img.size), Image.NEAREST)
+
+            diff_filename = 'diff_{0}.png'.format(int(time()))
+            with open(diff_filename, 'wb') as f:
+                diff_img.save(f, 'png')
+            f = discord.File(diff_filename, "diff.png")
+            if bad > 0:
+                content = getlang(ctx.guild.id, "render.diff_bad_color") \
+                    .format(tot - err, tot, err, bad, 100 * (tot - err) / tot)
+            else:
+                content = getlang(ctx.guild.id, "render.diff").format(tot - err, tot, err, 100 * (tot - err) / tot)
+            await ctx.channel.send(content=content, file=f)
+            os.remove(diff_filename)
+
+
+async def diff(ctx, x, y, att, zoom, fetch, colors):
     async with ctx.typing():
-        log.debug("Pixelcanvas preview invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
-                  .format(ctx.author, ctx.guild))
-
-        log.debug("X:{0} | Y:{1} | Zoom:{2} | IsExp: {3}".format(x, y, zoom, is_experimental))
-
-        # Coordinates of the template's top-left pixel with respect to the canvas
-        tlp = Coords(x - int(cfg.preview_w // 2), y - int(cfg.preview_h // 2))
-        # Offset of tlp from the top-left pixel of the chunk it is in
-        ch_off = Coords(tlp.x % 64, tlp.y % 64)
-        # Chunk coordinates of the big chunk that has tlp in its top left chunk
-        bc = Coords(int(tlp.x // 64) + 7, int(tlp.y // 64) + 7)
-        # Offset between tlp and the top-left pixel of where the preview should render from due to zooming
-        # z_off = Coords((cfg.preview_w / 2) * (1 - 1 / zoom), (cfg.preview_h / 2) * (1 - 1 / zoom))
-
-        async with aiohttp.ClientSession() as session:
-            url = pc_url_main if not is_experimental else pc_url_exp
-            async with session.get("{0}{1}.{2}.bmp".format(url, bc.x, bc.y)) as resp:
-                data = await resp.read()
-
-        # The second and fourth loops will repeat rows and columns to simulate magnification if zoom > 1
-        preview_img = Image.new('RGBA', (cfg.preview_w, cfg.preview_h), (255, 255, 255, 255))
-        for py in range(preview_img.height):
-            for px in range(preview_img.width):
-                i = __get_pixelcanvas_pixel_offset(ch_off, py, px)
-                color_id = data[int(i)] & 15 if i % 1 != 0 else data[int(i)] >> 4
-                color = pc_colors[color_id] + (255,)
-                preview_img.putpixel((px, py), color)
-
-        if zoom > 1:
-            preview_img = preview_img.resize(tuple(zoom*x for x in preview_img.size), Image.NEAREST)
-            tlp_z = Coords(preview_img.width//2-cfg.preview_w//2, preview_img.height//2-cfg.preview_h//2)
-            preview_img = preview_img.crop((tlp_z.x, tlp_z.y, tlp_z.x+cfg.preview_w, tlp_z.y+cfg.preview_h))
-
-        preview_filename = 'preview_{0}.png'.format(int(time()))
-        with open(preview_filename, 'wb') as f:
-            preview_img.save(f, 'png')
-        f = discord.File(preview_filename, "preview.png")
-        await ctx.channel.send(file=f)
-        os.remove(preview_filename)
-
-
-async def pixelcanvasio_diff(ctx, top_left_x, top_left_y, att, is_experimental):
-    async with ctx.typing():
-        log.debug("Pixelcanvas diff invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
-                  .format(ctx.author, ctx.guild))
-
         template_filename = 'template_{0}.png'.format(int(time()))
         with open(template_filename, 'wb') as f:
             await att.save(f)
         template = Image.open(template_filename).convert('RGBA')
         os.remove(template_filename)
 
-        log.debug("X:{0} | Y:{1} | Dim: {2}x{3} | IsExp: {4}"
-                  .format(top_left_x, top_left_y, template.width, template.height, is_experimental))
-
-        # Coordinates of the template's top-left pixel with respect to the canvas
-        tlp = Coords(top_left_x, top_left_y)
-        # Offset of tlp from the top-left pixel of the chunk it is in
-        ch_off = Coords(tlp.x % 64, tlp.y % 64)
-        # Chunk coordinates of the big chunk that has tlp in its top left chunk
-        bc = Coords(int(tlp.x // 64) + 7, int(tlp.y // 64) + 7)
-        # Number of extra big chunks spanned by the image after the initial top-left one
-        bc_ext = Coords(int((template.width + ch_off.x) // 960), int((template.height + ch_off.y) // 960))
-
-        if bc_ext.x > 0 or bc_ext.y > 0:
-            await ctx.channel.send("(Processing large template, this might take a few seconds...)")
-
-        async with aiohttp.ClientSession() as session:
-            url = pc_url_main if not is_experimental else pc_url_exp
-            data = bytes()
-            for bcy in range(bc_ext.y + 1):
-                for bcx in range(bc_ext.x + 1):
-                    async with session.get("{0}{1}.{2}.bmp".format(url, bc.x + 15 * bcx, bc.y + 15 * bcy)) as resp:
-                        data += await resp.read()
+        log.debug("X:{0} | Y:{1} | Dim: {2}x{3} | Zoom: {4}".format(x, y, template.width, template.height, zoom))
+
+        if template.width * template.height > 1000000:
+            await ctx.channel.send(getlang(ctx.guild.id, "render.large_template"))
+
+        diff_img = await fetch(x, y, template.width, template.height)
 
         tot = 0  # Total non-transparent pixels in template
         err = 0  # Number of errors
-        bad = 0  # Number of pixels in the template that are not in the Pixelcanvas color palette
-
-        diff_img = Image.new('RGBA', (template.width, template.height), (255, 255, 255, 255))
-        for py in range(diff_img.height):
-            for px in range(diff_img.width):
-                i = __get_pixelcanvas_pixel_offset(ch_off, py, px, bc_ext=bc_ext)
-                color_id = data[int(i)] & 15 if i % 1 != 0 else data[int(i)] >> 4
-                color = pc_colors[color_id]
+        bad = 0  # Number of pixels in the template that are not in the color palette
 
+        for py in range(template.height):
+            for px in range(template.width):
                 tp = template.getpixel((px, py))
+                dp = diff_img.getpixel((px, py))
                 if tp[3] is not 0:
                     # All non-transparent pixels count to the total
                     tot += 1
-                if 0 < tp[3] < 255 or (tp[3] is 255 and tp[:3] not in pc_colors):
+                if 0 < tp[3] < 255 or (tp[3] is 255 and tp[:3] not in colors):
                     # All non-opaque and non-transparent pixels, and opaque pixels of bad colors, are bad
                     pixel = (0, 0, 255, 255)
                     err += 1
                     bad += 1
-                elif tp[3] is 255 and (tp[0] is not color[0] or tp[1] is not color[1] or tp[2] is not color[2]):
+                elif tp[3] is 255 and (tp[0] is not dp[0] or tp[1] is not dp[1] or tp[2] is not dp[2]):
                     # All pixels that are valid and opaque but do not match the canvas are wrong
                     pixel = (255, 0, 0, 255)
                     err += 1
                 else:
                     # Render all correct/irrelevant pixels in greyscale
-                    avg = round(color[0] * 0.3 + color[1] * 0.52 + color[2] * 0.18)
+                    avg = round(dp[0] * 0.3 + dp[1] * 0.52 + dp[2] * 0.18)
                     pixel = (avg, avg, avg, 255)
 
                 diff_img.putpixel((px, py), pixel)
-                # if bad/tot > 0.75:
-                #     return
+
+        if zoom > 1:
+            diff_img = diff_img.resize(tuple(zoom * x for x in diff_img.size), Image.NEAREST)
 
         diff_filename = 'diff_{0}.png'.format(int(time()))
         with open(diff_filename, 'wb') as f:
             diff_img.save(f, 'png')
         f = discord.File(diff_filename, "diff.png")
         if bad > 0:
-            content = "{0}/{1} | {2} errors | {3} bad color | {4:.2f}% complete"\
+            content = getlang(ctx.guild.id, "render.diff_bad_color")\
                 .format(tot - err, tot, err, bad, 100 * (tot - err) / tot)
         else:
-            content = "{0}/{1} | {2} errors | {3:.2f}% complete".format(tot - err, tot, err, 100 * (tot - err) / tot)
+            content = getlang(ctx.guild.id, "render.diff").format(tot - err, tot, err, 100 * (tot - err) / tot)
         await ctx.channel.send(content=content, file=f)
         os.remove(diff_filename)
 
 
-def __get_pixelcanvas_pixel_offset(ch_off, y, x, bc_ext=Coords(1, 1)):
-    scan = Coords(ch_off.x + x, ch_off.y + y)
-    return (921600 * bc_ext.x * (scan.y // 960)  # Skips rows of big chunks
-            + 921600 * (scan.x // 960)           # Skips single big chunks in a row
-            + 4096 * 15 * (scan.y // 64)         # Skips rows of chunks in the big chunk
-            + 4096 * ((scan.x % 960) // 64)      # Skips single chunk in the row
-            + 64 * (scan.y % 64)                 # Skips rows of pixels in the chunk
-            + (scan.x % 64)                      # Skips single pixels in the row
-            ) / 2                                # Pixels come packed in pairs
+async def preview(ctx, x, y, zoom, fetch):
+    async with ctx.typing():
+        log.debug("X:{0} | Y:{1} | Zoom:{2}".format(x, y, zoom))
 
+        preview_img = await fetch(x - cfg.preview_w // 2, y - cfg.preview_h // 2, cfg.preview_w, cfg.preview_h)
 
-async def pixelzio_preview(ctx, x, y, zoom):
-    async with ctx.typing():
-        # Coordinates of the template's top-left pixel with respect to the canvas
-        tlp = Coords(x - int(cfg.preview_w // 2), y - int(cfg.preview_h // 2))
-        extension = Coords((tlp.x + cfg.preview_w) // 500 - tlp.x // 500, (tlp.y + cfg.preview_h) // 500 - tlp.y // 500)
-
-        async with aiohttp.ClientSession() as session:
-            uncropped_preview = Image.new('RGB', (500*(extension.x+1), 500*(extension.y+1)))
-            for iy in range(extension.y + 1):
-                for ix in range(extension.x + 1):
-                    async with session.get("{0}{1}_{2}/img".format(pixelz_url, (tlp.x // 500 + ix) * 500,
-                                                                   (tlp.y // 500 + iy) * 500)) as resp:
-                        data = await resp.read()
-                        tmp = Image.open(io.BytesIO(data)).convert('RGB')
-                        uncropped_preview.paste(tmp, (ix*500, iy*500, (ix*500)+500, (iy*500)+500))
-
-        preview = uncropped_preview.crop((tlp.x % 500, tlp.y % 500, (tlp.x % 500) + cfg.preview_w,
-                                          (tlp.y % 500) + cfg.preview_h))
         if zoom > 1:
-            preview = preview.resize(tuple(zoom*x for x in preview.size), Image.NEAREST)
-            tlp_z = Coords(preview.width//2-cfg.preview_w//2, preview.height//2-cfg.preview_h//2)
-            preview = preview.crop((tlp_z.x, tlp_z.y, tlp_z.x+cfg.preview_w, tlp_z.y+cfg.preview_h))
+            preview_img = preview_img.resize(tuple(zoom * x for x in preview_img.size), Image.NEAREST)
+            tlp_z = Coords(preview_img.width // 2 - cfg.preview_w // 2, preview_img.height // 2 - cfg.preview_h // 2)
+            preview_img = preview_img.crop((tlp_z.x, tlp_z.y, tlp_z.x + cfg.preview_w, tlp_z.y + cfg.preview_h))
 
         preview_filename = 'preview_{0}.png'.format(int(time()))
         with open(preview_filename, 'wb') as f:
-            preview.save(f, 'png')
+            preview_img.save(f, 'png')
         f = discord.File(preview_filename, "preview.png")
         await ctx.channel.send(file=f)
         os.remove(preview_filename)
 
 
-async def pixelzio_diff(ctx, top_left_x, top_left_y, att):
-    async with ctx.typing():
-        log.debug("Pixelz.io preview invoked by {0.name}#{0.discriminator} (ID: {0.id}) in {1.name} (ID: {1.id})"
-                  .format(ctx.author, ctx.guild))
-
-        template_filename = 'template_{0}.png'.format(int(time()))
-        with open(template_filename, 'wb') as f:
-            await att.save(f)
-        template = Image.open(template_filename).convert('RGBA')
-        os.remove(template_filename)
-
-        log.debug("X:{0} | Y:{1} | Dim: {2}x{3}"
-                  .format(top_left_x, top_left_y, template.width, template.height))
-
-        # Coordinates of the template's top-left pixel with respect to the canvas
-        tlp = Coords(top_left_x, top_left_y)
-        # # Number of extra big chunks spanned by the image after the initial top-left one
-        bc_ext = Coords((tlp.x + template.width) // 500 - tlp.x // 500, (tlp.y + template.height) // 500 - tlp.y // 500)
-
-        if bc_ext.x > 1 or bc_ext.y > 1:
-            await ctx.channel.send("(Processing large template, this might take a few seconds...)")
-
-        async with aiohttp.ClientSession() as session:
-            uncropped_preview = Image.new('RGB', (500*(bc_ext.x+1), 500*(bc_ext.y+1)))
-            for iy in range(bc_ext.y + 1):
-                for ix in range(bc_ext.x + 1):
-                    async with session.get("{0}{1}_{2}/img".format(pixelz_url, (tlp.x // 500 + ix) * 500,
-                                                                   (tlp.y // 500 + iy) * 500)) as resp:
-                        data = await resp.read()
-                        tmp = Image.open(io.BytesIO(data)).convert('RGB')
-                        uncropped_preview.paste(tmp, (ix*500, iy*500, (ix*500)+500, (iy*500)+500))
-
-        tot = 0  # Total non-transparent pixels in template
-        err = 0  # Number of errors
-        bad = 0  # Number of pixels in the template that are not in the Pixelcanvas color palette
-
-        tlp_off = Coords(tlp.x % 500, tlp.y % 500)
-        diff_img = Image.new('RGBA', (template.width, template.height), (255, 255, 255, 255))
-        for py in range(diff_img.height):
-            for px in range(diff_img.width):
-                tp = template.getpixel((px, py))
-                cp = uncropped_preview.getpixel((px + tlp_off.x, py + tlp_off.y))
-                if tp[3] is not 0:
-                    # All non-transparent pixels count to the total
-                    tot += 1
-                if 0 < tp[3] < 255 or (tp[3] is 255 and tp[:3] not in pixelz_colors):
-                    # All non-opaque and non-transparent pixels, and opaque pixels of bad colors, are bad
-                    pixel = (0, 0, 255, 255)
-                    err += 1
-                    bad += 1
-                elif tp[3] is 255 and (tp[0] is not cp[0] or tp[1] is not cp[1] or tp[2] is not cp[2]):
-                    # All pixels that are valid and opaque but do not match the canvas are wrong
-                    pixel = (255, 0, 0, 255)
-                    err += 1
-                else:
-                    # Render all correct/irrelevant pixels in greyscale
-                    avg = round(cp[0] * 0.3 + cp[1] * 0.52 + cp[2] * 0.18)
-                    pixel = (avg, avg, avg, 255)
-
-                diff_img.putpixel((px, py), pixel)
-                # if bad/tot > 0.75:
-                #     return
-
-        diff_filename = 'diff_{0}.png'.format(int(time()))
-        with open(diff_filename, 'wb') as f:
-            diff_img.save(f, 'png')
-        f = discord.File(diff_filename, "diff.png")
-        if bad > 0:
-            content = "{0}/{1} | {2} errors | {3} bad color | {4:.2f}% complete"\
-                .format(tot - err, tot, err, bad, 100 * (tot - err) / tot)
-        else:
-            content = "{0}/{1} | {2} errors | {3:.2f}% complete".format(tot - err, tot, err, 100 * (tot - err) / tot)
-        await ctx.channel.send(content=content, file=f)
-        os.remove(diff_filename)
+async def quantize(ctx, att, colors):
+    template_filename = 'template_{0}.png'.format(int(time()))
+    with open(template_filename, 'wb') as f:
+        await att.save(f)
+    template = Image.open(template_filename).convert('RGBA')
+    os.remove(template_filename)
+
+    log.debug("Dim: {0}x{1}".format(template.width, template.height))
+
+    bad_pixels = template.height * template.width
+    for py in range(template.height):
+        for px in range(template.width):
+            pix = template.getpixel((px, py))
+
+            if pix[3] == 0:  # Ignore fully transparent pixels
+                bad_pixels -= 1
+                continue
+            if pix[3] < 30:  # Make barely visible pixels transparent
+                template.putpixel((px, py), (0, 0, 0, 0))
+                continue
+
+            dist = 450
+            best_fit = (0, 0, 0)
+            for c in colors:
+                if pix[:3] == c:  # If pixel matches exactly, break
+                    best_fit = c
+                    if pix[3] == 255:  # Pixel is only not bad if it's fully opaque
+                        bad_pixels -= 1
+                    break
+                tmp = sqrt(pow(pix[0]-c[0], 2) + pow(pix[1]-c[1], 2) + pow(pix[2]-c[2], 2))
+                if tmp < dist:
+                    dist = tmp
+                    best_fit = c
+            template.putpixel((px, py), best_fit + (255,))
+
+    quantized_filename = 'cq_{0}.png'.format(int(time()))
+    with open(quantized_filename, 'wb') as f:
+        template.save(f, 'png')
+    f = discord.File(quantized_filename, att.filename)
+    await ctx.channel.send(getlang(ctx.guild.id, "render.quantize").format(bad_pixels), file=f)
+    os.remove(quantized_filename)
+
+
+async def fetch_pixelcanvas(x, y, dx, dy):
+    fetched = Image.new('RGB', (dx, dy), (255, 255, 255))
+    ch_off = Coords(x % 64, y % 64)
+    bc = Coords(x // 64 + 7, y // 64 + 7)
+    bc_ext = Coords((dx + ch_off.x) // 960 + 1, (dy + ch_off.y) // 960 + 1)
+
+    async with aiohttp.ClientSession() as session:
+        for iy in range(0, bc_ext.y * 15, 15):
+            for ix in range(0, bc_ext.x * 15, 15):
+                url = "http://pixelcanvas.io/api/bigchunk/{0}.{1}.bmp".format(bc.x + ix, bc.y + iy)
+                headers = {"Accept-Encoding": "gzip"}
+                async with session.get(url, headers=headers) as resp:
+                    data = await resp.read()
+
+    def pixel_to_data_index():
+        scan = Coords(ch_off.x + px, ch_off.y + py)
+        return (921600 * bc_ext.x * (scan.y // 960)  # Skips rows of big chunks
+                + 921600 * (scan.x // 960)           # Skips single big chunks in a row
+                + 4096 * 15 * (scan.y // 64)         # Skips rows of chunks in the big chunk
+                + 4096 * ((scan.x % 960) // 64)      # Skips single chunk in the row
+                + 64 * (scan.y % 64)                 # Skips rows of pixels in the chunk
+                + (scan.x % 64)                      # Skips single pixels in the row
+                ) / 2                                # Pixels come packed in pairs
+
+    for py in range(dy):
+        for px in range(dx):
+            i = pixel_to_data_index()
+            color_id = data[int(i)] & 15 if i % 1 != 0 else data[int(i)] >> 4
+            color = pc_colors[color_id] + (255,)
+            fetched.putpixel((px, py), color)
+
+    return fetched
+
+
+async def fetch_pixelzio(x, y, dx, dy):
+    chk = Coords((x // 500) * 500, (y // 500) * 500)
+    ext = Coords((x + dx) // 500 - x // 500 + 1, (y + dy) // 500 - y // 500 + 1)
+    fetched = Image.new('RGB', (500 * ext.x, 500 * ext.y))
+
+    async with aiohttp.ClientSession() as session:
+        for iy in range(0, ext.y * 500, 500):
+            for ix in range(0, ext.x * 500, 500):
+                url = "http://pixelz.io/api/{0}_{1}/img".format(chk.x + ix, chk.y + iy)
+                headers = {"Accept-Encoding": "gzip"}
+                async with session.get(url, headers=headers) as resp:
+                    data = await resp.read()
+                    tmp = Image.open(io.BytesIO(data)).convert('RGB')
+                    fetched.paste(tmp, (ix, iy, ix + 500, iy + 500))
+
+    return fetched.crop((x % 500, y % 500, (x % 500) + dx, (y % 500) + dy))
diff --git a/utils/sqlite.py b/utils/sqlite.py
index 3c3abfe..0a688f7 100644
--- a/utils/sqlite.py
+++ b/utils/sqlite.py
@@ -17,12 +17,21 @@ def create_tables():
                         alert_channel INTEGER,
                         emojishare INTEGER NOT NULL DEFAULT 0,
                         autoscan INTEGER NOT NULL DEFAULT 1,
-                        default_canvas TEXT NOT NULL DEFAULT "pixelcanvas.io"
+                        default_canvas TEXT NOT NULL DEFAULT "pixelcanvas.io",
+                        language TEXT NOT NULL DEFAULT "en_US"
                     )""")
     c.execute("""CREATE TABLE IF NOT EXISTS version(id INTEGER PRIMARY KEY CHECK (id = 1), version REAL)""")
+
     c.execute("""CREATE TABLE IF NOT EXISTS animote_users(id INTEGER)""")
 
 
+def update_tables(v):
+    # print("updating tables... "+v)
+    if v is not None:
+        if v < 1.2:
+            c.execute("""ALTER TABLE guilds ADD COLUMN language TEXT NOT NULL DEFAULT "en_US" """)
+
+
 def add_guild(gid, name, join_date):
     c.execute("""INSERT INTO guilds(id, name, join_date) VALUES(?, ?, ?)""", (gid, name, join_date))
     conn.commit()
@@ -41,7 +50,8 @@ def get_all_guilds():
     return c.fetchall()
 
 
-def update_guild(gid, name=None, prefix=None, alert_channel=None, emojishare=None, autoscan=None, default_canvas=None):
+def update_guild(gid, name=None, prefix=None, alert_channel=None, emojishare=None, autoscan=None, default_canvas=None,
+                 language=None):
     if name is not None:
         c.execute("""UPDATE guilds SET name=? WHERE id=?""", (name, gid))
     if prefix is not None:
@@ -54,6 +64,8 @@ def update_guild(gid, name=None, prefix=None, alert_channel=None, emojishare=Non
         c.execute("""UPDATE guilds SET autoscan=? WHERE id=?""", (autoscan, gid))
     if default_canvas is not None:
         c.execute("""UPDATE guilds SET default_canvas=? WHERE id=?""", (default_canvas, gid))
+    if language is not None:
+        c.execute("""UPDATE guilds SET language=? WHERE id=?""", (language, gid))
     conn.commit()
 
 
@@ -70,7 +82,14 @@ def get_version():
     return result[0]
 
 
+def init_version(version):
+    print("version initialized to {}".format(version))
+    c.execute("""INSERT INTO version(version) VALUES(?)""", (version,))
+    conn.commit()
+
+
 def update_version(version):
+    print("updated to {}".format(version))
     c.execute("""UPDATE version SET version=?""", (version,))
     conn.commit()
 
@@ -95,4 +114,10 @@ def is_server_emojishare_server(gid):
     return c.fetchone() is not None
 
 
+def get_guild_language(gid):
+    c.execute("""SELECT language FROM guilds WHERE id=?""", (gid,))
+    return c.fetchone()[0]
+
+
 create_tables()
+update_tables(get_version())
diff --git a/utils/version.py b/utils/version.py
index 6a1d680..390c5f7 100644
--- a/utils/version.py
+++ b/utils/version.py
@@ -1,2 +1,2 @@
-VERSION = 1.1
+VERSION = 1.2
 AUTHORS = ["DiamondIceNS"]