[Scummvm-git-logs] scummvm master -> 4229e2642ef33f761528cc5811726df56f3a42d0

criezy criezy at scummvm.org
Sun Jun 3 18:41:52 CEST 2018


This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
af0434cf09 GRAPHICS: Implement LarryScale cel scaling algorithm
f8c32b07e7 SCI: Use LarryScale cel scaler instead of nearest-neighbor
999cf71dcf SCI: Add game option for enabling/disabling LarryScale in LSL7
4229e2642e Merge pull request #1141 from DanielSWolf/larryscale


Commit: af0434cf091c868ad55ce8885872a147fbd22262
    https://github.com/scummvm/scummvm/commit/af0434cf091c868ad55ce8885872a147fbd22262
Author: Daniel Wolf (dwolf at dannad.de)
Date: 2018-03-16T22:33:06+01:00

Commit Message:
GRAPHICS: Implement LarryScale cel scaling algorithm

Changed paths:
  A graphics/larryScale.cpp
  A graphics/larryScale.h
  A graphics/larryScale_generated.cpp
  A graphics/larryScale_generator.js
    graphics/module.mk


diff --git a/graphics/larryScale.cpp b/graphics/larryScale.cpp
new file mode 100644
index 0000000..c4b2f6e
--- /dev/null
+++ b/graphics/larryScale.cpp
@@ -0,0 +1,406 @@
+/* ScummVM - Graphic Adventure Engine
+*
+* ScummVM is the legal property of its developers, whose names
+* are too numerous to list here. Please refer to the COPYRIGHT
+* file distributed with this source distribution.
+*
+* 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 the Free Software Foundation; either version 2
+* of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*
+*/
+
+#include "larryScale.h"
+#include <cassert>
+#include "common/array.h"
+#include <cstring>
+
+namespace Graphics {
+
+typedef LarryScaleColor Color;
+
+const int kMargin = 2;
+
+// A bitmap that has a margin of `kMargin` pixels all around it.
+// Allows fast access without time-consuming bounds checking.
+template<typename T>
+class MarginedBitmap {
+	int _width;
+	int _height;
+	int _stride;
+	Common::Array<T> _buffer;
+	T *_origin;
+
+public:
+	MarginedBitmap(int width, int height, T marginValue) :
+		_width(width),
+		_height(height),
+		_stride(width + 2 * kMargin),
+		_buffer(_stride * (height + 2 * kMargin)),
+		_origin(calculateOrigin())
+	{
+		fillMargin(marginValue);
+	}
+
+	// We need a custom copy constructor.
+	// Otherwise, _origin would point to the original buffer.
+	MarginedBitmap(const MarginedBitmap &rhs) :
+		_width(rhs._width),
+		_height(rhs._height),
+		_stride(rhs._stride),
+		_buffer(rhs._buffer),
+		_origin(calculateOrigin())
+	{}
+
+	// We need a custom assignment operator.
+	// Otherwise, _origin would point to the original buffer.
+	MarginedBitmap &operator =(const MarginedBitmap &rhs) {
+		_width = rhs._width;
+		_height = rhs._height;
+		_stride = rhs._stride;
+		_buffer = rhs._buffer;
+		_origin = calculateOrigin();
+		return this;
+	}
+
+	int getWidth() const { return _width; }
+	int getHeight() const { return _height; }
+	int getStride() const { return _stride; }
+	const T *getOrigin() const { return _origin; }
+	T *getOrigin() { return _origin; }
+
+	const T *getPointerTo(int x, int y) const {
+		return _origin + y * _stride + x;
+	}
+	T *getPointerTo(int x, int y) {
+		return _origin + y * _stride + x;
+	}
+
+	T get(int x, int y) const {
+		return _origin[y * _stride + x];
+	}
+	void set(int x, int y, T value) {
+		_origin[y * _stride + x] = value;
+	}
+
+	void fillMargin(T value);
+
+private:
+	T *calculateOrigin() {
+		return _buffer.data() + kMargin * _stride + kMargin;
+	}
+};
+
+template<typename T>
+void MarginedBitmap<T>::fillMargin(T value) {
+	T * const data = getOrigin();
+	const int stride = getStride();
+
+	// Fill top margin
+	for (int y = -kMargin; y < 0; ++y) {
+		for (int x = -kMargin; x < _width + kMargin; ++x) {
+			data[y * stride + x] = value;
+		}
+	}
+
+	// Fill sideways margins
+	for (int y = 0; y < _height; ++y) {
+		for (int x = -kMargin; x < 0; ++x) {
+			data[y * stride + x] = value;
+		}
+		for (int x = _width; x < _width + kMargin; ++x) {
+			data[y * stride + x] = value;
+		}
+	}
+
+	// Fill bottom margin
+	for (int y = _height; y < _height + kMargin; ++y) {
+		for (int x = -kMargin; x < _width + kMargin; ++x) {
+			data[y * stride + x] = value;
+		}
+	}
+}
+
+MarginedBitmap<Color> createMarginedBitmap(int width, int height, Color marginColor, RowReader &rowReader) {
+	MarginedBitmap<Color> result(width, height, marginColor);
+	for (int y = 0; y < height; ++y) {
+		memcpy(result.getPointerTo(0, y), rowReader.readRow(y), width * sizeof(Color));
+	}
+	return result;
+}
+
+class MarginedBitmapWriter : public RowWriter {
+	MarginedBitmap<Color> &_target;
+public:
+	explicit MarginedBitmapWriter(MarginedBitmap<Color> &target)
+		: _target(target) {}
+
+	void writeRow(int y, const LarryScaleColor *row) {
+		memcpy(_target.getPointerTo(0, y), row, _target.getWidth() * sizeof(Color));
+	}
+};
+
+inline bool isLinePixel(const MarginedBitmap<Color> &src, int x, int y) {
+#define EQUALS(xOffset, yOffset) (src.get(x + xOffset, y + yOffset) == pixel)
+
+	const Color pixel = src.get(x, y);
+
+	// Single pixels are fills
+	if (!EQUALS(-1, -1) && !EQUALS(0, -1) && !EQUALS(1, -1) && !EQUALS(1, 0) && !EQUALS(1, 1) && !EQUALS(0, 1) && !EQUALS(-1, 1) && !EQUALS(-1, 0)) {
+		return false;
+	}
+
+	// 2x2 blocks are fills
+	if (EQUALS(0, -1) && EQUALS(1, -1) && EQUALS(1, 0)) return false;
+	if (EQUALS(1, 0) && EQUALS(1, 1) && EQUALS(0, 1)) return false;
+	if (EQUALS(0, 1) && EQUALS(-1, 1) && EQUALS(-1, 0)) return false;
+	if (EQUALS(-1, 0) && EQUALS(-1, -1) && EQUALS(0, -1)) return false;
+
+	// A pixel adjacent to a 2x2 block is a fill.
+	if (EQUALS(-1, -1) && EQUALS(0, -1) && EQUALS(-1, -2) && EQUALS(0, -2)) return false;
+	if (EQUALS(0, -1) && EQUALS(1, -1) && EQUALS(0, -2) && EQUALS(1, -2)) return false;
+	if (EQUALS(1, -1) && EQUALS(1, 0) && EQUALS(2, -1) && EQUALS(2, 0)) return false;
+	if (EQUALS(1, 0) && EQUALS(1, 1) && EQUALS(2, 0) && EQUALS(2, 1)) return false;
+	if (EQUALS(1, 1) && EQUALS(0, 1) && EQUALS(1, 2) && EQUALS(0, 2)) return false;
+	if (EQUALS(0, 1) && EQUALS(-1, 1) && EQUALS(0, 2) && EQUALS(-1, 2)) return false;
+	if (EQUALS(-1, 1) && EQUALS(-1, 0) && EQUALS(-2, 1) && EQUALS(-2, 0)) return false;
+	if (EQUALS(-1, 0) && EQUALS(-1, -1) && EQUALS(-2, 0) && EQUALS(-2, -1)) return false;
+
+	// Everything else is part of a line
+	return true;
+
+#undef EQUALS
+}
+
+MarginedBitmap<bool> createMarginedLinePixelsBitmap(const MarginedBitmap<Color> &src) {
+	MarginedBitmap<bool> result(src.getWidth(), src.getHeight(), false);
+	for (int y = 0; y < src.getHeight(); ++y) {
+		for (int x = 0; x < src.getWidth(); ++x) {
+			result.set(x, y, isLinePixel(src, x, y));
+		}
+	}
+	return result;
+}
+
+void scaleDown(
+	const MarginedBitmap<Color> &src,
+	Color transparentColor,
+	int dstWidth, int dstHeight,
+	RowWriter &rowWriter
+) {
+	assert(src.getWidth() > 0);
+	assert(src.getHeight() > 0);
+	assert(dstWidth > 0 && dstWidth <= src.getWidth());
+	assert(dstHeight > 0 && dstHeight <= src.getHeight());
+
+	Common::Array<Color> dstRow(dstWidth);
+	for (int dstY = 0; dstY < dstHeight; ++dstY) {
+		const int srcY1 = dstY * src.getHeight() / dstHeight;
+		const int srcY2 = (dstY + 1) * src.getHeight() / dstHeight;
+
+		for (int dstX = 0; dstX < dstWidth; ++dstX) {
+			const int srcX1 = dstX * src.getWidth() / dstWidth;
+			const int srcX2 = (dstX + 1) * src.getWidth() / dstWidth;
+
+			const int blockPixelCount = (srcX2 - srcX1) * (srcY2 - srcY1);
+			if (blockPixelCount <= 4) {
+				// Downscaling to 50% or more. Prefer line pixels.
+				Color bestLineColor = 0;
+				int linePixelCount = 0;
+				for (int srcY = srcY1; srcY < srcY2; ++srcY) {
+					for (int srcX = srcX1; srcX < srcX2; ++srcX) {
+						const bool colorIsFromLine = isLinePixel(src, srcX, srcY);
+						if (colorIsFromLine) {
+							bestLineColor = src.get(srcX, srcY);
+							++linePixelCount;
+						}
+					}
+				}
+				const bool sufficientLinePixels = linePixelCount * 2 >= blockPixelCount;
+				const Color resultColor = sufficientLinePixels
+					? bestLineColor
+					: src.get(srcX1, srcY1);
+				dstRow[dstX] = resultColor;
+			} else {
+				// Downscaling significantly. Prefer outline pixels.
+				Color bestColor = src.get(srcX1, srcY1);
+				for (int srcY = srcY1; srcY < srcY2; ++srcY) {
+					for (int srcX = srcX1; srcX < srcX2; ++srcX) {
+						const Color pixelColor = src.get(srcX, srcY);
+						const bool isOutlinePixel = pixelColor != transparentColor && (
+							src.get(srcX - 1, srcY) == transparentColor
+							|| src.get(srcX + 1, srcY) == transparentColor
+							|| src.get(srcX, srcY - 1) == transparentColor
+							|| src.get(srcX, srcY + 1) == transparentColor
+							);
+						if (isOutlinePixel) {
+							bestColor = pixelColor;
+							goto foundOutlinePixel;
+						}
+					}
+				}
+			foundOutlinePixel:
+				dstRow[dstX] = bestColor;
+			}
+		}
+		rowWriter.writeRow(dstY, dstRow.data());
+	}
+}
+
+// An equality matrix is a combination of eight Boolean flags indicating whether
+// each of the surrounding pixels has the same color as the central pixel.
+//
+// +------+------+------+
+// | 0x02 | 0x04 | 0x08 |
+// +------+------+------+
+// | 0x01 | Ref. | 0x10 |
+// +------+------+------+
+// | 0x80 | 0x40 | 0x20 |
+// +------+------+------+
+typedef byte EqualityMatrix;
+
+EqualityMatrix getEqualityMatrix(const Color *pixel, int stride) {
+#define EQUALS(x, y) (pixel[y * stride + x] == *pixel)
+
+	return (EQUALS(-1, 0) ? 0x01 : 0x00)
+		| (EQUALS(-1, -1) ? 0x02 : 0x00)
+		| (EQUALS(0, -1) ? 0x04 : 0x00)
+		| (EQUALS(1, -1) ? 0x08 : 0x00)
+		| (EQUALS(1, 0) ? 0x10 : 0x00)
+		| (EQUALS(1, 1) ? 0x20 : 0x00)
+		| (EQUALS(0, 1) ? 0x40 : 0x00)
+		| (EQUALS(-1, 1) ? 0x80 : 0x00);
+
+#undef EQUALS
+}
+
+// scapeUp() requires generated functions
+#include "larryScale_generated.cpp"
+
+void scaleUp(
+	const MarginedBitmap<Color> &src,
+	int dstWidth, int dstHeight,
+	RowWriter &rowWriter
+) {
+	const int srcWidth = src.getWidth();
+	const int srcHeight = src.getHeight();
+
+	assert(srcWidth > 0);
+	assert(srcHeight > 0);
+	assert(dstWidth >= srcWidth && dstWidth <= 2 * src.getWidth());
+	assert(dstHeight >= srcHeight && dstHeight <= 2 * src.getHeight());
+
+	const MarginedBitmap<bool> linePixels = createMarginedLinePixelsBitmap(src);
+	Common::Array<Color> topDstRow(dstWidth);
+	Common::Array<Color> bottomDstRow(dstWidth);
+	for (int srcY = 0; srcY < src.getHeight(); ++srcY) {
+		const int dstY1 = srcY * dstHeight / src.getHeight();
+		const int dstY2 = (srcY + 1) * dstHeight / src.getHeight();
+		const int dstBlockHeight = dstY2 - dstY1;
+
+		for (int srcX = 0; srcX < src.getWidth(); ++srcX) {
+			const int dstX1 = srcX * dstWidth / src.getWidth();
+			const int dstX2 = (srcX + 1) * dstWidth / src.getWidth();
+			const int dstBlockWidth = dstX2 - dstX1;
+
+			if (dstBlockWidth == 1) {
+				if (dstBlockHeight == 1) {
+					// 1x1
+					topDstRow[dstX1] = src.get(srcX, srcY);
+				} else {
+					// 1x2
+					Color &top = topDstRow[dstX1];
+					Color &bottom = bottomDstRow[dstX1];
+					scalePixelTo1x2(src, linePixels, srcX, srcY, top, bottom);
+				}
+			} else {
+				if (dstBlockHeight == 1) {
+					// 2x1
+					Color &left = topDstRow[dstX1];
+					Color &right = topDstRow[dstX1 + 1];
+					scalePixelTo2x1(src, linePixels, srcX, srcY, left, right);
+				} else {
+					// 2x2
+					Color &topLeft = topDstRow[dstX1];
+					Color &topRight = topDstRow[dstX1 + 1];
+					Color &bottomLeft = bottomDstRow[dstX1];
+					Color &bottomRight = bottomDstRow[dstX1 + 1];
+					scalePixelTo2x2(src, linePixels, srcX, srcY, topLeft, topRight, bottomLeft, bottomRight);
+				}
+			}
+		}
+		rowWriter.writeRow(dstY1, topDstRow.data());
+		if (dstBlockHeight == 2) {
+			rowWriter.writeRow(dstY1 + 1, bottomDstRow.data());
+		}
+	}
+}
+
+void copyRows(int height, RowReader &rowReader, RowWriter &rowWriter) {
+	for (int y = 0; y < height; ++y) {
+		rowWriter.writeRow(y, rowReader.readRow(y));
+	}
+}
+
+void larryScale(
+	const MarginedBitmap<Color> &src,
+	Color transparentColor,
+	int dstWidth, int dstHeight,
+	RowWriter &rowWriter
+) {
+	const int srcWidth = src.getWidth();
+	const int srcHeight = src.getHeight();
+
+	if (
+		(dstWidth > srcWidth && dstHeight < srcHeight)		// Upscaling along x axis, downscaling along y axis
+		|| (dstWidth < srcWidth && dstHeight > srcHeight)	// Downscaling along x axis, upscaling along y axis
+		|| (dstWidth > 2 * srcWidth)						// Upscaling to more than 200% along x axis
+		|| (dstHeight > 2 * srcHeight)						// Upscaling to more than 200% along y axis
+	) {
+		// We can't handle these cases with a single upscale.
+		// Let's do an intermediate scale.
+		const int tmpWidth = CLIP(dstWidth, srcWidth, 2 * srcWidth);
+		const int tmpHeight = CLIP(dstHeight, srcHeight, 2 * srcHeight);
+		MarginedBitmap<Color> tmp(tmpWidth, tmpHeight, transparentColor);
+		MarginedBitmapWriter writer = MarginedBitmapWriter(tmp);
+		larryScale(src, transparentColor, tmpWidth, tmpHeight, writer);
+		larryScale(tmp, transparentColor, dstWidth, dstHeight, rowWriter);
+	} else if (dstWidth > srcWidth || dstHeight > srcHeight) {
+		// Upscaling to no more than 200%
+		scaleUp(src, dstWidth, dstHeight, rowWriter);
+	} else {
+		// Downscaling
+		scaleDown(src, transparentColor, dstWidth, dstHeight, rowWriter);
+	}
+}
+
+void larryScale(
+	int srcWidth, int srcHeight,
+	Color transparentColor,
+	RowReader &rowReader,
+	int dstWidth, int dstHeight,
+	RowWriter &rowWriter
+) {
+	// Select the appropriate scaler
+	if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) {
+		// Nothing to do
+	} else if (dstWidth == srcWidth && dstHeight == srcHeight) {
+		copyRows(srcHeight, rowReader, rowWriter);
+	} else {
+		const MarginedBitmap<Color> src =
+			createMarginedBitmap(srcWidth, srcHeight, transparentColor, rowReader);
+		larryScale(src, transparentColor, dstWidth, dstHeight, rowWriter);
+	}
+}
+
+}
diff --git a/graphics/larryScale.h b/graphics/larryScale.h
new file mode 100644
index 0000000..0248cdb
--- /dev/null
+++ b/graphics/larryScale.h
@@ -0,0 +1,82 @@
+/* ScummVM - Graphic Adventure Engine
+*
+* ScummVM is the legal property of its developers, whose names
+* are too numerous to list here. Please refer to the COPYRIGHT
+* file distributed with this source distribution.
+*
+* 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 the Free Software Foundation; either version 2
+* of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*
+*/
+
+#ifndef GRAPHICS_LARRYSCALE_H
+#define GRAPHICS_LARRYSCALE_H
+
+#include "common/scummsys.h"
+
+namespace Graphics {
+
+/*
+
+LarryScale is a bitmap scaler for cartoon images.
+
+Features:
+
+* Supports arbitrary scaling factors along both axes (but looks best between 50% and 200%).
+* When downscaling: preserves fine details.
+* When upscaling: keeps lines smooth, introducing very few block artifacts.
+* Does not introduce additional colors.
+
+Limitations:
+
+* Lines must not be anti-aliased and should be only one pixel wide.
+* Fills should be flat without gradients.
+
+*/
+
+typedef byte LarryScaleColor;
+
+class RowReader {
+public:
+	virtual const LarryScaleColor *readRow(int y) = 0;
+	virtual ~RowReader() {}
+};
+
+class RowWriter {
+public:
+	virtual void writeRow(int y, const LarryScaleColor *row) = 0;
+	virtual ~RowWriter() {}
+};
+
+/**
+ * @param srcWidth			The width, in pixels, of the original image
+ * @param srcHeight			The height, in pixels, of the original image
+ * @param transparentColor	The transparent color. Used for outline detection.
+ * @param rowReader			An object with a callback method for reading the lines of the original
+ *							image
+ * @param dstWidth			The width, in pixels, of the scaled target image
+ * @param dstHeight			The height, in pixels, of the scaled target image
+ * @param rowWriter			An object with a callback method accepting the lines of the target image
+ */
+void larryScale(
+	int srcWidth, int srcHeight,
+	LarryScaleColor transparentColor,
+	RowReader &rowReader,
+	int dstWidth, int dstHeight,
+	RowWriter &rowWriter
+);
+
+}
+
+#endif
diff --git a/graphics/larryScale_generated.cpp b/graphics/larryScale_generated.cpp
new file mode 100644
index 0000000..f987648
--- /dev/null
+++ b/graphics/larryScale_generated.cpp
@@ -0,0 +1,639 @@
+/* ScummVM - Graphic Adventure Engine
+*
+* ScummVM is the legal property of its developers, whose names
+* are too numerous to list here. Please refer to the COPYRIGHT
+* file distributed with this source distribution.
+*
+* 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 the Free Software Foundation; either version 2
+* of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*
+*/
+
+// This file was generated by larryScale_generator.js.
+// Do not edit directly! Instead, edit the generator script and run it.
+
+inline void scalePixelTo2x2(
+	const MarginedBitmap<Color> &src,
+	const MarginedBitmap<bool> &linePixels,
+	int x, int y,
+	// Out parameters
+	Color &topLeft, Color &topRight, Color &bottomLeft, Color &bottomRight
+) {
+	const Color pixel = src.get(x, y);
+	const EqualityMatrix matrix = getEqualityMatrix(src.getPointerTo(x, y), src.getStride());
+
+	// Note: There is a case label for every possible value, so we don't need a default label.
+	switch (matrix) {
+	case 0x00 /*⠀⠂⠀*/:	case 0x07 /*⠃⠃⠀*/:	case 0x17 /*⠃⠃⠂*/:	case 0x1c /*⠀⠃⠃*/:	case 0x1d /*⠂⠃⠃*/:	case 0x1e /*⠁⠃⠃*/:	case 0x1f /*⠃⠃⠃*/:	case 0x27 /*⠃⠃⠄*/:
+	case 0x2f /*⠃⠃⠅*/:	case 0x37 /*⠃⠃⠆*/:	case 0x3c /*⠀⠃⠇*/:	case 0x3d /*⠂⠃⠇*/:	case 0x3e /*⠁⠃⠇*/:	case 0x3f /*⠃⠃⠇*/:	case 0x47 /*⠃⠇⠀*/:	case 0x4f /*⠃⠇⠁*/:
+	case 0x50 /*⠀⠆⠂*/:	case 0x51 /*⠂⠆⠂*/:	case 0x52 /*⠁⠆⠂*/:	case 0x54 /*⠀⠇⠂*/:	case 0x55 /*⠂⠇⠂*/:	case 0x57 /*⠃⠇⠂*/:	case 0x5c /*⠀⠇⠃*/:	case 0x5d /*⠂⠇⠃*/:
+	case 0x5e /*⠁⠇⠃*/:	case 0x5f /*⠃⠇⠃*/:	case 0x67 /*⠃⠇⠄*/:	case 0x6f /*⠃⠇⠅*/:	case 0x70 /*⠀⠆⠆*/:	case 0x71 /*⠂⠆⠆*/:	case 0x72 /*⠁⠆⠆*/:	case 0x73 /*⠃⠆⠆*/:
+	case 0x74 /*⠀⠇⠆*/:	case 0x75 /*⠂⠇⠆*/:	case 0x76 /*⠁⠇⠆*/:	case 0x77 /*⠃⠇⠆*/:	case 0x78 /*⠀⠆⠇*/:	case 0x79 /*⠂⠆⠇*/:	case 0x7a /*⠁⠆⠇*/:	case 0x7b /*⠃⠆⠇*/:
+	case 0x7c /*⠀⠇⠇*/:	case 0x7d /*⠂⠇⠇*/:	case 0x7e /*⠁⠇⠇*/:	case 0x7f /*⠃⠇⠇*/:	case 0x97 /*⠇⠃⠂*/:	case 0x9c /*⠄⠃⠃*/:	case 0x9d /*⠆⠃⠃*/:	case 0x9e /*⠅⠃⠃*/:
+	case 0x9f /*⠇⠃⠃*/:	case 0xa7 /*⠇⠃⠄*/:	case 0xaa /*⠅⠂⠅*/:	case 0xaf /*⠇⠃⠅*/:	case 0xb7 /*⠇⠃⠆*/:	case 0xbc /*⠄⠃⠇*/:	case 0xbd /*⠆⠃⠇*/:	case 0xbe /*⠅⠃⠇*/:
+	case 0xbf /*⠇⠃⠇*/:	case 0xc1 /*⠆⠆⠀*/:	case 0xc5 /*⠆⠇⠀*/:	case 0xc7 /*⠇⠇⠀*/:	case 0xc9 /*⠆⠆⠁*/:	case 0xcb /*⠇⠆⠁*/:	case 0xcd /*⠆⠇⠁*/:	case 0xcf /*⠇⠇⠁*/:
+	case 0xd1 /*⠆⠆⠂*/:	case 0xd3 /*⠇⠆⠂*/:	case 0xd5 /*⠆⠇⠂*/:	case 0xd7 /*⠇⠇⠂*/:	case 0xd9 /*⠆⠆⠃*/:	case 0xdb /*⠇⠆⠃*/:	case 0xdc /*⠄⠇⠃*/:	case 0xdd /*⠆⠇⠃*/:
+	case 0xde /*⠅⠇⠃*/:	case 0xdf /*⠇⠇⠃*/:	case 0xe5 /*⠆⠇⠄*/:	case 0xe7 /*⠇⠇⠄*/:	case 0xe9 /*⠆⠆⠅*/:	case 0xeb /*⠇⠆⠅*/:	case 0xed /*⠆⠇⠅*/:	case 0xef /*⠇⠇⠅*/:
+	case 0xf0 /*⠄⠆⠆*/:	case 0xf1 /*⠆⠆⠆*/:	case 0xf2 /*⠅⠆⠆*/:	case 0xf3 /*⠇⠆⠆*/:	case 0xf4 /*⠄⠇⠆*/:	case 0xf5 /*⠆⠇⠆*/:	case 0xf6 /*⠅⠇⠆*/:	case 0xf7 /*⠇⠇⠆*/:
+	case 0xf8 /*⠄⠆⠇*/:	case 0xf9 /*⠆⠆⠇*/:	case 0xfa /*⠅⠆⠇*/:	case 0xfb /*⠇⠆⠇*/:	case 0xfc /*⠄⠇⠇*/:	case 0xfd /*⠆⠇⠇*/:	case 0xfe /*⠅⠇⠇*/:	case 0xff /*⠇⠇⠇*/:
+		topLeft = topRight = bottomLeft = bottomRight = pixel;
+		break;
+	case 0xa4 /*⠄⠃⠄*/:
+		topLeft = bottomLeft = bottomRight = pixel;
+		topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x29 /*⠂⠂⠅*/:
+		topLeft = topRight = bottomRight = pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0x92 /*⠅⠂⠂*/:	case 0x94 /*⠄⠃⠂*/:
+		topLeft = topRight = bottomLeft = pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0x44 /*⠀⠇⠀*/:	case 0x45 /*⠂⠇⠀*/:
+		topLeft = bottomLeft = pixel;
+		topRight = bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x11 /*⠂⠂⠂*/:	case 0x15 /*⠂⠃⠂*/:
+		topLeft = topRight = pixel;
+		bottomLeft = bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0x49 /*⠂⠆⠁*/:	case 0x4a /*⠁⠆⠁*/:
+		topLeft = topRight = bottomLeft = pixel;
+		bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x53 /*⠃⠆⠂*/:	case 0x56 /*⠁⠇⠂*/:	case 0x59 /*⠂⠆⠃*/:	case 0x5a /*⠁⠆⠃*/:	case 0x5b /*⠃⠆⠃*/:	case 0xd2 /*⠅⠆⠂*/:	case 0xd4 /*⠄⠇⠂*/:	case 0xd6 /*⠅⠇⠂*/:
+	case 0xda /*⠅⠆⠃*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0xa8 /*⠄⠂⠅*/:
+		topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		topRight = bottomLeft = bottomRight = pixel;
+		break;
+	case 0x8a /*⠅⠂⠁*/:	case 0x8f /*⠇⠃⠁*/:
+		topLeft = topRight = bottomLeft = pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x2a /*⠁⠂⠅*/:
+		topLeft = topRight = bottomRight = pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		break;
+	case 0xa2 /*⠅⠂⠄*/:	case 0xe3 /*⠇⠆⠄*/:
+		topLeft = bottomLeft = bottomRight = pixel;
+		topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x25 /*⠂⠃⠄*/:
+		topLeft = bottomRight = pixel;
+		topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0xa6 /*⠅⠃⠄*/:	case 0xac /*⠄⠃⠅*/:	case 0xae /*⠅⠃⠅*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x13 /*⠃⠂⠂*/:	case 0x19 /*⠂⠂⠃*/:	case 0x1b /*⠃⠂⠃*/:	case 0x31 /*⠂⠂⠆*/:	case 0x33 /*⠃⠂⠆*/:	case 0x35 /*⠂⠃⠆*/:	case 0x39 /*⠂⠂⠇*/:	case 0x3b /*⠃⠂⠇*/:
+	case 0x91 /*⠆⠂⠂*/:	case 0x93 /*⠇⠂⠂*/:	case 0x95 /*⠆⠃⠂*/:	case 0x99 /*⠆⠂⠃*/:	case 0x9b /*⠇⠂⠃*/:	case 0xb1 /*⠆⠂⠆*/:	case 0xb3 /*⠇⠂⠆*/:	case 0xb5 /*⠆⠃⠆*/:
+	case 0xb9 /*⠆⠂⠇*/:	case 0xbb /*⠇⠂⠇*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = pixel;
+			bottomLeft = bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x96 /*⠅⠃⠂*/:	case 0x9a /*⠅⠂⠃*/:	case 0xb2 /*⠅⠂⠆*/:	case 0xb4 /*⠄⠃⠆*/:	case 0xb6 /*⠅⠃⠆*/:	case 0xba /*⠅⠂⠇*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = bottomLeft = pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x4b /*⠃⠆⠁*/:	case 0x69 /*⠂⠆⠅*/:	case 0x6a /*⠁⠆⠅*/:	case 0x6b /*⠃⠆⠅*/:	case 0xca /*⠅⠆⠁*/:	case 0xea /*⠅⠆⠅*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = bottomLeft = pixel;
+			bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x2b /*⠃⠂⠅*/:	case 0xa9 /*⠆⠂⠅*/:	case 0xab /*⠇⠂⠅*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = bottomRight = pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x46 /*⠁⠇⠀*/:	case 0x4c /*⠀⠇⠁*/:	case 0x4d /*⠂⠇⠁*/:	case 0x4e /*⠁⠇⠁*/:	case 0x64 /*⠀⠇⠄*/:	case 0x65 /*⠂⠇⠄*/:	case 0x66 /*⠁⠇⠄*/:	case 0x6c /*⠀⠇⠅*/:
+	case 0x6d /*⠂⠇⠅*/:	case 0x6e /*⠁⠇⠅*/:	case 0xc4 /*⠄⠇⠀*/:	case 0xc6 /*⠅⠇⠀*/:	case 0xcc /*⠄⠇⠁*/:	case 0xce /*⠅⠇⠁*/:	case 0xe4 /*⠄⠇⠄*/:	case 0xe6 /*⠅⠇⠄*/:
+	case 0xec /*⠄⠇⠅*/:	case 0xee /*⠅⠇⠅*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomLeft = pixel;
+			topRight = bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x24 /*⠀⠃⠄*/:
+		topLeft = !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		topRight = bottomRight = pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		break;
+	case 0x04 /*⠀⠃⠀*/:	case 0x84 /*⠄⠃⠀*/:	case 0x87 /*⠇⠃⠀*/:
+		topLeft = bottomLeft = pixel;
+		topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x90 /*⠄⠂⠂*/:
+		topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : pixel;
+		bottomLeft = bottomRight = pixel;
+		break;
+	case 0x21 /*⠂⠂⠄*/:	case 0xe1 /*⠆⠆⠄*/:
+		topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : pixel;
+		topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomLeft = bottomRight = pixel;
+		break;
+	case 0x48 /*⠀⠆⠁*/:
+		topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		topRight = bottomRight = pixel;
+		bottomLeft = !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		break;
+	case 0x01 /*⠂⠂⠀*/:	case 0x09 /*⠂⠂⠁*/:	case 0x0f /*⠃⠃⠁*/:
+		topLeft = topRight = pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x10 /*⠀⠂⠂*/:	case 0x12 /*⠁⠂⠂*/:	case 0x14 /*⠀⠃⠂*/:
+		topLeft = topRight = pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0x40 /*⠀⠆⠀*/:	case 0x41 /*⠂⠆⠀*/:	case 0x42 /*⠁⠆⠀*/:	case 0xc3 /*⠇⠆⠀*/:
+		topLeft = bottomLeft = pixel;
+		topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0xd8 /*⠄⠆⠃*/:
+		if (linePixels.get(x, y)) {
+			topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			topRight = bottomLeft = bottomRight = pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0xa0 /*⠄⠂⠄*/:
+		topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomLeft = bottomRight = pixel;
+		break;
+	case 0x0a /*⠁⠂⠁*/:
+		topLeft = topRight = pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x08 /*⠀⠂⠁*/:	case 0x80 /*⠄⠂⠀*/:	case 0x88 /*⠄⠂⠁*/:
+		topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		topRight = bottomLeft = pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x28 /*⠀⠂⠅*/:
+		topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		topRight = bottomRight = pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		break;
+	case 0x02 /*⠁⠂⠀*/:	case 0x20 /*⠀⠂⠄*/:	case 0x22 /*⠁⠂⠄*/:
+		topLeft = bottomRight = pixel;
+		topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		break;
+	case 0x82 /*⠅⠂⠀*/:
+		topLeft = bottomLeft = pixel;
+		topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x2d /*⠂⠃⠅*/:	case 0xa5 /*⠆⠃⠄*/:	case 0xad /*⠆⠃⠅*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x05 /*⠂⠃⠀*/:
+		topLeft = pixel;
+		topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x68 /*⠀⠆⠅*/:	case 0xc8 /*⠄⠆⠁*/:	case 0xe8 /*⠄⠆⠅*/:
+		if (linePixels.get(x, y)) {
+			topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			topRight = bottomLeft = pixel;
+			bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x26 /*⠁⠃⠄*/:	case 0x2c /*⠀⠃⠅*/:	case 0x2e /*⠁⠃⠅*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0xc0 /*⠄⠆⠀*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomLeft = pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x18 /*⠀⠂⠃*/:	case 0x1a /*⠁⠂⠃*/:	case 0x30 /*⠀⠂⠆*/:	case 0x32 /*⠁⠂⠆*/:	case 0x36 /*⠁⠃⠆*/:	case 0x38 /*⠀⠂⠇*/:	case 0x3a /*⠁⠂⠇*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x06 /*⠁⠃⠀*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomLeft = pixel;
+			topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x98 /*⠄⠂⠃*/:	case 0xb0 /*⠄⠂⠆*/:	case 0xb8 /*⠄⠂⠇*/:
+		if (linePixels.get(x, y)) {
+			topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			topRight = bottomLeft = pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x58 /*⠀⠆⠃*/:	case 0xd0 /*⠄⠆⠂*/:
+		if (linePixels.get(x, y)) {
+			topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : pixel;
+			bottomLeft = !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			bottomRight = pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x16 /*⠁⠃⠂*/:	case 0x34 /*⠀⠃⠆*/:
+		if (linePixels.get(x, y)) {
+			topLeft = !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			topRight = pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = bottomRight = pixel;
+		}
+		break;
+	case 0x60 /*⠀⠆⠄*/:	case 0x62 /*⠁⠆⠄*/:	case 0x63 /*⠃⠆⠄*/:	case 0xc2 /*⠅⠆⠀*/:	case 0xe0 /*⠄⠆⠄*/:	case 0xe2 /*⠅⠆⠄*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomLeft = pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = bottomLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		}
+		break;
+	case 0x23 /*⠃⠂⠄*/:	case 0xa1 /*⠆⠂⠄*/:	case 0xa3 /*⠇⠂⠄*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			topLeft = bottomLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		}
+		break;
+	case 0x0b /*⠃⠂⠁*/:	case 0x81 /*⠆⠂⠀*/:	case 0x83 /*⠇⠂⠀*/:	case 0x89 /*⠆⠂⠁*/:	case 0x8b /*⠇⠂⠁*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		}
+		break;
+	case 0x03 /*⠃⠂⠀*/:
+		if (linePixels.get(x, y)) {
+			topLeft = topRight = pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = bottomLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		}
+		break;
+	case 0x0c /*⠀⠃⠁*/:	case 0x0e /*⠁⠃⠁*/:	case 0x86 /*⠅⠃⠀*/:	case 0x8c /*⠄⠃⠁*/:	case 0x8e /*⠅⠃⠁*/:
+		if (linePixels.get(x, y)) {
+			topLeft = bottomLeft = pixel;
+			topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		}
+		break;
+	case 0x0d /*⠂⠃⠁*/:	case 0x85 /*⠆⠃⠀*/:	case 0x8d /*⠆⠃⠁*/:
+		if (linePixels.get(x, y)) {
+			topLeft = pixel;
+			topRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomLeft = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = topRight = bottomLeft = pixel;
+			bottomRight = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		}
+		break;
+	case 0x43 /*⠃⠆⠀*/:	case 0x61 /*⠂⠆⠄*/:
+		if (linePixels.get(x, y)) {
+			topLeft = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottomLeft = pixel;
+			bottomRight = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			topLeft = bottomLeft = bottomRight = pixel;
+			topRight = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		}
+		break;
+	}
+}
+
+inline void scalePixelTo2x1(
+	const MarginedBitmap<Color> &src,
+	const MarginedBitmap<bool> &linePixels,
+	int x, int y,
+	// Out parameters
+	Color &left, Color &right
+) {
+	const Color pixel = src.get(x, y);
+	const EqualityMatrix matrix = getEqualityMatrix(src.getPointerTo(x, y), src.getStride());
+
+	// Note: There is a case label for every possible value, so we don't need a default label.
+	switch (matrix) {
+	case 0x00 /*⠀⠂⠀*/:	case 0x01 /*⠂⠂⠀*/:	case 0x02 /*⠁⠂⠀*/:	case 0x07 /*⠃⠃⠀*/:	case 0x08 /*⠀⠂⠁*/:	case 0x09 /*⠂⠂⠁*/:	case 0x0a /*⠁⠂⠁*/:	case 0x0f /*⠃⠃⠁*/:
+	case 0x10 /*⠀⠂⠂*/:	case 0x11 /*⠂⠂⠂*/:	case 0x12 /*⠁⠂⠂*/:	case 0x14 /*⠀⠃⠂*/:	case 0x15 /*⠂⠃⠂*/:	case 0x17 /*⠃⠃⠂*/:	case 0x1c /*⠀⠃⠃*/:	case 0x1d /*⠂⠃⠃*/:
+	case 0x1e /*⠁⠃⠃*/:	case 0x1f /*⠃⠃⠃*/:	case 0x20 /*⠀⠂⠄*/:	case 0x21 /*⠂⠂⠄*/:	case 0x22 /*⠁⠂⠄*/:	case 0x25 /*⠂⠃⠄*/:	case 0x27 /*⠃⠃⠄*/:	case 0x29 /*⠂⠂⠅*/:
+	case 0x2a /*⠁⠂⠅*/:	case 0x2f /*⠃⠃⠅*/:	case 0x37 /*⠃⠃⠆*/:	case 0x3c /*⠀⠃⠇*/:	case 0x3d /*⠂⠃⠇*/:	case 0x3e /*⠁⠃⠇*/:	case 0x3f /*⠃⠃⠇*/:	case 0x47 /*⠃⠇⠀*/:
+	case 0x49 /*⠂⠆⠁*/:	case 0x4a /*⠁⠆⠁*/:	case 0x4f /*⠃⠇⠁*/:	case 0x50 /*⠀⠆⠂*/:	case 0x51 /*⠂⠆⠂*/:	case 0x52 /*⠁⠆⠂*/:	case 0x54 /*⠀⠇⠂*/:	case 0x55 /*⠂⠇⠂*/:
+	case 0x57 /*⠃⠇⠂*/:	case 0x5c /*⠀⠇⠃*/:	case 0x5d /*⠂⠇⠃*/:	case 0x5e /*⠁⠇⠃*/:	case 0x5f /*⠃⠇⠃*/:	case 0x67 /*⠃⠇⠄*/:	case 0x6f /*⠃⠇⠅*/:	case 0x70 /*⠀⠆⠆*/:
+	case 0x71 /*⠂⠆⠆*/:	case 0x72 /*⠁⠆⠆*/:	case 0x73 /*⠃⠆⠆*/:	case 0x74 /*⠀⠇⠆*/:	case 0x75 /*⠂⠇⠆*/:	case 0x76 /*⠁⠇⠆*/:	case 0x77 /*⠃⠇⠆*/:	case 0x78 /*⠀⠆⠇*/:
+	case 0x79 /*⠂⠆⠇*/:	case 0x7a /*⠁⠆⠇*/:	case 0x7b /*⠃⠆⠇*/:	case 0x7c /*⠀⠇⠇*/:	case 0x7d /*⠂⠇⠇*/:	case 0x7e /*⠁⠇⠇*/:	case 0x7f /*⠃⠇⠇*/:	case 0x80 /*⠄⠂⠀*/:
+	case 0x88 /*⠄⠂⠁*/:	case 0x8a /*⠅⠂⠁*/:	case 0x8f /*⠇⠃⠁*/:	case 0x90 /*⠄⠂⠂*/:	case 0x92 /*⠅⠂⠂*/:	case 0x94 /*⠄⠃⠂*/:	case 0x97 /*⠇⠃⠂*/:	case 0x9c /*⠄⠃⠃*/:
+	case 0x9d /*⠆⠃⠃*/:	case 0x9e /*⠅⠃⠃*/:	case 0x9f /*⠇⠃⠃*/:	case 0xa0 /*⠄⠂⠄*/:	case 0xa2 /*⠅⠂⠄*/:	case 0xa4 /*⠄⠃⠄*/:	case 0xa7 /*⠇⠃⠄*/:	case 0xa8 /*⠄⠂⠅*/:
+	case 0xaa /*⠅⠂⠅*/:	case 0xaf /*⠇⠃⠅*/:	case 0xb7 /*⠇⠃⠆*/:	case 0xbc /*⠄⠃⠇*/:	case 0xbd /*⠆⠃⠇*/:	case 0xbe /*⠅⠃⠇*/:	case 0xbf /*⠇⠃⠇*/:	case 0xc1 /*⠆⠆⠀*/:
+	case 0xc5 /*⠆⠇⠀*/:	case 0xc7 /*⠇⠇⠀*/:	case 0xc9 /*⠆⠆⠁*/:	case 0xcb /*⠇⠆⠁*/:	case 0xcd /*⠆⠇⠁*/:	case 0xcf /*⠇⠇⠁*/:	case 0xd1 /*⠆⠆⠂*/:	case 0xd3 /*⠇⠆⠂*/:
+	case 0xd5 /*⠆⠇⠂*/:	case 0xd7 /*⠇⠇⠂*/:	case 0xd9 /*⠆⠆⠃*/:	case 0xdb /*⠇⠆⠃*/:	case 0xdc /*⠄⠇⠃*/:	case 0xdd /*⠆⠇⠃*/:	case 0xde /*⠅⠇⠃*/:	case 0xdf /*⠇⠇⠃*/:
+	case 0xe1 /*⠆⠆⠄*/:	case 0xe3 /*⠇⠆⠄*/:	case 0xe5 /*⠆⠇⠄*/:	case 0xe7 /*⠇⠇⠄*/:	case 0xe9 /*⠆⠆⠅*/:	case 0xeb /*⠇⠆⠅*/:	case 0xed /*⠆⠇⠅*/:	case 0xef /*⠇⠇⠅*/:
+	case 0xf0 /*⠄⠆⠆*/:	case 0xf1 /*⠆⠆⠆*/:	case 0xf2 /*⠅⠆⠆*/:	case 0xf3 /*⠇⠆⠆*/:	case 0xf4 /*⠄⠇⠆*/:	case 0xf5 /*⠆⠇⠆*/:	case 0xf6 /*⠅⠇⠆*/:	case 0xf7 /*⠇⠇⠆*/:
+	case 0xf8 /*⠄⠆⠇*/:	case 0xf9 /*⠆⠆⠇*/:	case 0xfa /*⠅⠆⠇*/:	case 0xfb /*⠇⠆⠇*/:	case 0xfc /*⠄⠇⠇*/:	case 0xfd /*⠆⠇⠇*/:	case 0xfe /*⠅⠇⠇*/:	case 0xff /*⠇⠇⠇*/:
+		left = right = pixel;
+		break;
+	case 0x44 /*⠀⠇⠀*/:	case 0x45 /*⠂⠇⠀*/:
+		left = pixel;
+		right = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x03 /*⠃⠂⠀*/:	case 0x0b /*⠃⠂⠁*/:	case 0x13 /*⠃⠂⠂*/:	case 0x18 /*⠀⠂⠃*/:	case 0x19 /*⠂⠂⠃*/:	case 0x1a /*⠁⠂⠃*/:	case 0x1b /*⠃⠂⠃*/:	case 0x23 /*⠃⠂⠄*/:
+	case 0x26 /*⠁⠃⠄*/:	case 0x2b /*⠃⠂⠅*/:	case 0x2c /*⠀⠃⠅*/:	case 0x2d /*⠂⠃⠅*/:	case 0x2e /*⠁⠃⠅*/:	case 0x30 /*⠀⠂⠆*/:	case 0x31 /*⠂⠂⠆*/:	case 0x32 /*⠁⠂⠆*/:
+	case 0x33 /*⠃⠂⠆*/:	case 0x35 /*⠂⠃⠆*/:	case 0x36 /*⠁⠃⠆*/:	case 0x38 /*⠀⠂⠇*/:	case 0x39 /*⠂⠂⠇*/:	case 0x3a /*⠁⠂⠇*/:	case 0x3b /*⠃⠂⠇*/:	case 0x4b /*⠃⠆⠁*/:
+	case 0x53 /*⠃⠆⠂*/:	case 0x56 /*⠁⠇⠂*/:	case 0x59 /*⠂⠆⠃*/:	case 0x5a /*⠁⠆⠃*/:	case 0x5b /*⠃⠆⠃*/:	case 0x68 /*⠀⠆⠅*/:	case 0x69 /*⠂⠆⠅*/:	case 0x6a /*⠁⠆⠅*/:
+	case 0x6b /*⠃⠆⠅*/:	case 0x81 /*⠆⠂⠀*/:	case 0x83 /*⠇⠂⠀*/:	case 0x89 /*⠆⠂⠁*/:	case 0x8b /*⠇⠂⠁*/:	case 0x91 /*⠆⠂⠂*/:	case 0x93 /*⠇⠂⠂*/:	case 0x95 /*⠆⠃⠂*/:
+	case 0x96 /*⠅⠃⠂*/:	case 0x98 /*⠄⠂⠃*/:	case 0x99 /*⠆⠂⠃*/:	case 0x9a /*⠅⠂⠃*/:	case 0x9b /*⠇⠂⠃*/:	case 0xa1 /*⠆⠂⠄*/:	case 0xa3 /*⠇⠂⠄*/:	case 0xa5 /*⠆⠃⠄*/:
+	case 0xa6 /*⠅⠃⠄*/:	case 0xa9 /*⠆⠂⠅*/:	case 0xab /*⠇⠂⠅*/:	case 0xac /*⠄⠃⠅*/:	case 0xad /*⠆⠃⠅*/:	case 0xae /*⠅⠃⠅*/:	case 0xb0 /*⠄⠂⠆*/:	case 0xb1 /*⠆⠂⠆*/:
+	case 0xb2 /*⠅⠂⠆*/:	case 0xb3 /*⠇⠂⠆*/:	case 0xb4 /*⠄⠃⠆*/:	case 0xb5 /*⠆⠃⠆*/:	case 0xb6 /*⠅⠃⠆*/:	case 0xb8 /*⠄⠂⠇*/:	case 0xb9 /*⠆⠂⠇*/:	case 0xba /*⠅⠂⠇*/:
+	case 0xbb /*⠇⠂⠇*/:	case 0xc8 /*⠄⠆⠁*/:	case 0xca /*⠅⠆⠁*/:	case 0xd2 /*⠅⠆⠂*/:	case 0xd4 /*⠄⠇⠂*/:	case 0xd6 /*⠅⠇⠂*/:	case 0xd8 /*⠄⠆⠃*/:	case 0xda /*⠅⠆⠃*/:
+	case 0xe8 /*⠄⠆⠅*/:	case 0xea /*⠅⠆⠅*/:
+		if (linePixels.get(x, y)) {
+			left = right = pixel;
+		} else {
+			left = right = pixel;
+		}
+		break;
+	case 0x48 /*⠀⠆⠁*/:
+		left = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		right = pixel;
+		break;
+	case 0x40 /*⠀⠆⠀*/:	case 0x41 /*⠂⠆⠀*/:	case 0x42 /*⠁⠆⠀*/:	case 0xc3 /*⠇⠆⠀*/:
+		left = pixel;
+		right = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x24 /*⠀⠃⠄*/:
+		left = !linePixels.get(x - 1, y) ? src.get(x - 1, y) : !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		right = pixel;
+		break;
+	case 0x04 /*⠀⠃⠀*/:	case 0x05 /*⠂⠃⠀*/:	case 0x84 /*⠄⠃⠀*/:	case 0x87 /*⠇⠃⠀*/:
+		left = pixel;
+		right = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0x46 /*⠁⠇⠀*/:	case 0x4c /*⠀⠇⠁*/:	case 0x4d /*⠂⠇⠁*/:	case 0x4e /*⠁⠇⠁*/:	case 0x64 /*⠀⠇⠄*/:	case 0x65 /*⠂⠇⠄*/:	case 0x66 /*⠁⠇⠄*/:	case 0x6c /*⠀⠇⠅*/:
+	case 0x6d /*⠂⠇⠅*/:	case 0x6e /*⠁⠇⠅*/:	case 0xc4 /*⠄⠇⠀*/:	case 0xc6 /*⠅⠇⠀*/:	case 0xcc /*⠄⠇⠁*/:	case 0xce /*⠅⠇⠁*/:	case 0xe4 /*⠄⠇⠄*/:	case 0xe6 /*⠅⠇⠄*/:
+	case 0xec /*⠄⠇⠅*/:	case 0xee /*⠅⠇⠅*/:
+		if (linePixels.get(x, y)) {
+			left = pixel;
+			right = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			left = right = pixel;
+		}
+		break;
+	case 0x28 /*⠀⠂⠅*/:
+		left = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		right = pixel;
+		break;
+	case 0x82 /*⠅⠂⠀*/:
+		left = pixel;
+		right = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0x43 /*⠃⠆⠀*/:	case 0x60 /*⠀⠆⠄*/:	case 0x61 /*⠂⠆⠄*/:	case 0x62 /*⠁⠆⠄*/:	case 0x63 /*⠃⠆⠄*/:	case 0xc0 /*⠄⠆⠀*/:	case 0xc2 /*⠅⠆⠀*/:	case 0xe0 /*⠄⠆⠄*/:
+	case 0xe2 /*⠅⠆⠄*/:
+		if (linePixels.get(x, y)) {
+			left = pixel;
+			right = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			left = right = pixel;
+		}
+		break;
+	case 0x16 /*⠁⠃⠂*/:	case 0x34 /*⠀⠃⠆*/:
+		if (linePixels.get(x, y)) {
+			left = !linePixels.get(x - 1, y) ? src.get(x - 1, y) : !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+			right = pixel;
+		} else {
+			left = right = pixel;
+		}
+		break;
+	case 0x06 /*⠁⠃⠀*/:	case 0x0c /*⠀⠃⠁*/:	case 0x0d /*⠂⠃⠁*/:	case 0x0e /*⠁⠃⠁*/:	case 0x85 /*⠆⠃⠀*/:	case 0x86 /*⠅⠃⠀*/:	case 0x8c /*⠄⠃⠁*/:	case 0x8d /*⠆⠃⠁*/:
+	case 0x8e /*⠅⠃⠁*/:
+		if (linePixels.get(x, y)) {
+			left = pixel;
+			right = !linePixels.get(x + 1, y) ? src.get(x + 1, y) : !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			left = right = pixel;
+		}
+		break;
+	case 0x58 /*⠀⠆⠃*/:	case 0xd0 /*⠄⠆⠂*/:
+		if (linePixels.get(x, y)) {
+			left = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			right = pixel;
+		} else {
+			left = right = pixel;
+		}
+		break;
+	}
+}
+
+inline void scalePixelTo1x2(
+	const MarginedBitmap<Color> &src,
+	const MarginedBitmap<bool> &linePixels,
+	int x, int y,
+	// Out parameters
+	Color &top, Color &bottom
+) {
+	const Color pixel = src.get(x, y);
+	const EqualityMatrix matrix = getEqualityMatrix(src.getPointerTo(x, y), src.getStride());
+
+	// Note: There is a case label for every possible value, so we don't need a default label.
+	switch (matrix) {
+	case 0x00 /*⠀⠂⠀*/:	case 0x02 /*⠁⠂⠀*/:	case 0x04 /*⠀⠃⠀*/:	case 0x07 /*⠃⠃⠀*/:	case 0x08 /*⠀⠂⠁*/:	case 0x17 /*⠃⠃⠂*/:	case 0x1c /*⠀⠃⠃*/:	case 0x1d /*⠂⠃⠃*/:
+	case 0x1e /*⠁⠃⠃*/:	case 0x1f /*⠃⠃⠃*/:	case 0x20 /*⠀⠂⠄*/:	case 0x22 /*⠁⠂⠄*/:	case 0x24 /*⠀⠃⠄*/:	case 0x25 /*⠂⠃⠄*/:	case 0x27 /*⠃⠃⠄*/:	case 0x28 /*⠀⠂⠅*/:
+	case 0x29 /*⠂⠂⠅*/:	case 0x2a /*⠁⠂⠅*/:	case 0x2f /*⠃⠃⠅*/:	case 0x37 /*⠃⠃⠆*/:	case 0x3c /*⠀⠃⠇*/:	case 0x3d /*⠂⠃⠇*/:	case 0x3e /*⠁⠃⠇*/:	case 0x3f /*⠃⠃⠇*/:
+	case 0x40 /*⠀⠆⠀*/:	case 0x41 /*⠂⠆⠀*/:	case 0x42 /*⠁⠆⠀*/:	case 0x44 /*⠀⠇⠀*/:	case 0x45 /*⠂⠇⠀*/:	case 0x47 /*⠃⠇⠀*/:	case 0x48 /*⠀⠆⠁*/:	case 0x49 /*⠂⠆⠁*/:
+	case 0x4a /*⠁⠆⠁*/:	case 0x4f /*⠃⠇⠁*/:	case 0x50 /*⠀⠆⠂*/:	case 0x51 /*⠂⠆⠂*/:	case 0x52 /*⠁⠆⠂*/:	case 0x54 /*⠀⠇⠂*/:	case 0x55 /*⠂⠇⠂*/:	case 0x57 /*⠃⠇⠂*/:
+	case 0x5c /*⠀⠇⠃*/:	case 0x5d /*⠂⠇⠃*/:	case 0x5e /*⠁⠇⠃*/:	case 0x5f /*⠃⠇⠃*/:	case 0x67 /*⠃⠇⠄*/:	case 0x6f /*⠃⠇⠅*/:	case 0x70 /*⠀⠆⠆*/:	case 0x71 /*⠂⠆⠆*/:
+	case 0x72 /*⠁⠆⠆*/:	case 0x73 /*⠃⠆⠆*/:	case 0x74 /*⠀⠇⠆*/:	case 0x75 /*⠂⠇⠆*/:	case 0x76 /*⠁⠇⠆*/:	case 0x77 /*⠃⠇⠆*/:	case 0x78 /*⠀⠆⠇*/:	case 0x79 /*⠂⠆⠇*/:
+	case 0x7a /*⠁⠆⠇*/:	case 0x7b /*⠃⠆⠇*/:	case 0x7c /*⠀⠇⠇*/:	case 0x7d /*⠂⠇⠇*/:	case 0x7e /*⠁⠇⠇*/:	case 0x7f /*⠃⠇⠇*/:	case 0x80 /*⠄⠂⠀*/:	case 0x82 /*⠅⠂⠀*/:
+	case 0x84 /*⠄⠃⠀*/:	case 0x87 /*⠇⠃⠀*/:	case 0x88 /*⠄⠂⠁*/:	case 0x8a /*⠅⠂⠁*/:	case 0x8f /*⠇⠃⠁*/:	case 0x92 /*⠅⠂⠂*/:	case 0x94 /*⠄⠃⠂*/:	case 0x97 /*⠇⠃⠂*/:
+	case 0x9c /*⠄⠃⠃*/:	case 0x9d /*⠆⠃⠃*/:	case 0x9e /*⠅⠃⠃*/:	case 0x9f /*⠇⠃⠃*/:	case 0xa2 /*⠅⠂⠄*/:	case 0xa4 /*⠄⠃⠄*/:	case 0xa7 /*⠇⠃⠄*/:	case 0xa8 /*⠄⠂⠅*/:
+	case 0xaa /*⠅⠂⠅*/:	case 0xaf /*⠇⠃⠅*/:	case 0xb7 /*⠇⠃⠆*/:	case 0xbc /*⠄⠃⠇*/:	case 0xbd /*⠆⠃⠇*/:	case 0xbe /*⠅⠃⠇*/:	case 0xbf /*⠇⠃⠇*/:	case 0xc1 /*⠆⠆⠀*/:
+	case 0xc3 /*⠇⠆⠀*/:	case 0xc5 /*⠆⠇⠀*/:	case 0xc7 /*⠇⠇⠀*/:	case 0xc9 /*⠆⠆⠁*/:	case 0xcb /*⠇⠆⠁*/:	case 0xcd /*⠆⠇⠁*/:	case 0xcf /*⠇⠇⠁*/:	case 0xd1 /*⠆⠆⠂*/:
+	case 0xd3 /*⠇⠆⠂*/:	case 0xd5 /*⠆⠇⠂*/:	case 0xd7 /*⠇⠇⠂*/:	case 0xd9 /*⠆⠆⠃*/:	case 0xdb /*⠇⠆⠃*/:	case 0xdc /*⠄⠇⠃*/:	case 0xdd /*⠆⠇⠃*/:	case 0xde /*⠅⠇⠃*/:
+	case 0xdf /*⠇⠇⠃*/:	case 0xe3 /*⠇⠆⠄*/:	case 0xe5 /*⠆⠇⠄*/:	case 0xe7 /*⠇⠇⠄*/:	case 0xe9 /*⠆⠆⠅*/:	case 0xeb /*⠇⠆⠅*/:	case 0xed /*⠆⠇⠅*/:	case 0xef /*⠇⠇⠅*/:
+	case 0xf0 /*⠄⠆⠆*/:	case 0xf1 /*⠆⠆⠆*/:	case 0xf2 /*⠅⠆⠆*/:	case 0xf3 /*⠇⠆⠆*/:	case 0xf4 /*⠄⠇⠆*/:	case 0xf5 /*⠆⠇⠆*/:	case 0xf6 /*⠅⠇⠆*/:	case 0xf7 /*⠇⠇⠆*/:
+	case 0xf8 /*⠄⠆⠇*/:	case 0xf9 /*⠆⠆⠇*/:	case 0xfa /*⠅⠆⠇*/:	case 0xfb /*⠇⠆⠇*/:	case 0xfc /*⠄⠇⠇*/:	case 0xfd /*⠆⠇⠇*/:	case 0xfe /*⠅⠇⠇*/:	case 0xff /*⠇⠇⠇*/:
+		top = bottom = pixel;
+		break;
+	case 0x11 /*⠂⠂⠂*/:	case 0x15 /*⠂⠃⠂*/:
+		top = pixel;
+		bottom = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		break;
+	case 0x06 /*⠁⠃⠀*/:	case 0x0c /*⠀⠃⠁*/:	case 0x0e /*⠁⠃⠁*/:	case 0x23 /*⠃⠂⠄*/:	case 0x26 /*⠁⠃⠄*/:	case 0x2b /*⠃⠂⠅*/:	case 0x2c /*⠀⠃⠅*/:	case 0x2d /*⠂⠃⠅*/:
+	case 0x2e /*⠁⠃⠅*/:	case 0x46 /*⠁⠇⠀*/:	case 0x4b /*⠃⠆⠁*/:	case 0x4c /*⠀⠇⠁*/:	case 0x4d /*⠂⠇⠁*/:	case 0x4e /*⠁⠇⠁*/:	case 0x53 /*⠃⠆⠂*/:	case 0x56 /*⠁⠇⠂*/:
+	case 0x59 /*⠂⠆⠃*/:	case 0x5a /*⠁⠆⠃*/:	case 0x5b /*⠃⠆⠃*/:	case 0x60 /*⠀⠆⠄*/:	case 0x62 /*⠁⠆⠄*/:	case 0x63 /*⠃⠆⠄*/:	case 0x64 /*⠀⠇⠄*/:	case 0x65 /*⠂⠇⠄*/:
+	case 0x66 /*⠁⠇⠄*/:	case 0x68 /*⠀⠆⠅*/:	case 0x69 /*⠂⠆⠅*/:	case 0x6a /*⠁⠆⠅*/:	case 0x6b /*⠃⠆⠅*/:	case 0x6c /*⠀⠇⠅*/:	case 0x6d /*⠂⠇⠅*/:	case 0x6e /*⠁⠇⠅*/:
+	case 0x86 /*⠅⠃⠀*/:	case 0x8c /*⠄⠃⠁*/:	case 0x8e /*⠅⠃⠁*/:	case 0x96 /*⠅⠃⠂*/:	case 0x98 /*⠄⠂⠃*/:	case 0x9a /*⠅⠂⠃*/:	case 0xa1 /*⠆⠂⠄*/:	case 0xa3 /*⠇⠂⠄*/:
+	case 0xa5 /*⠆⠃⠄*/:	case 0xa6 /*⠅⠃⠄*/:	case 0xa9 /*⠆⠂⠅*/:	case 0xab /*⠇⠂⠅*/:	case 0xac /*⠄⠃⠅*/:	case 0xad /*⠆⠃⠅*/:	case 0xae /*⠅⠃⠅*/:	case 0xb0 /*⠄⠂⠆*/:
+	case 0xb2 /*⠅⠂⠆*/:	case 0xb4 /*⠄⠃⠆*/:	case 0xb6 /*⠅⠃⠆*/:	case 0xb8 /*⠄⠂⠇*/:	case 0xba /*⠅⠂⠇*/:	case 0xc0 /*⠄⠆⠀*/:	case 0xc2 /*⠅⠆⠀*/:	case 0xc4 /*⠄⠇⠀*/:
+	case 0xc6 /*⠅⠇⠀*/:	case 0xc8 /*⠄⠆⠁*/:	case 0xca /*⠅⠆⠁*/:	case 0xcc /*⠄⠇⠁*/:	case 0xce /*⠅⠇⠁*/:	case 0xd2 /*⠅⠆⠂*/:	case 0xd4 /*⠄⠇⠂*/:	case 0xd6 /*⠅⠇⠂*/:
+	case 0xd8 /*⠄⠆⠃*/:	case 0xda /*⠅⠆⠃*/:	case 0xe0 /*⠄⠆⠄*/:	case 0xe2 /*⠅⠆⠄*/:	case 0xe4 /*⠄⠇⠄*/:	case 0xe6 /*⠅⠇⠄*/:	case 0xe8 /*⠄⠆⠅*/:	case 0xea /*⠅⠆⠅*/:
+	case 0xec /*⠄⠇⠅*/:	case 0xee /*⠅⠇⠅*/:
+		if (linePixels.get(x, y)) {
+			top = bottom = pixel;
+		} else {
+			top = bottom = pixel;
+		}
+		break;
+	case 0x90 /*⠄⠂⠂*/:
+		top = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		bottom = pixel;
+		break;
+	case 0x21 /*⠂⠂⠄*/:	case 0xe1 /*⠆⠆⠄*/:
+		top = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottom = pixel;
+		break;
+	case 0x10 /*⠀⠂⠂*/:	case 0x12 /*⠁⠂⠂*/:	case 0x14 /*⠀⠃⠂*/:
+		top = pixel;
+		bottom = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		break;
+	case 0x01 /*⠂⠂⠀*/:	case 0x05 /*⠂⠃⠀*/:	case 0x09 /*⠂⠂⠁*/:	case 0x0f /*⠃⠃⠁*/:
+		top = pixel;
+		bottom = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0x13 /*⠃⠂⠂*/:	case 0x19 /*⠂⠂⠃*/:	case 0x1b /*⠃⠂⠃*/:	case 0x31 /*⠂⠂⠆*/:	case 0x33 /*⠃⠂⠆*/:	case 0x35 /*⠂⠃⠆*/:	case 0x39 /*⠂⠂⠇*/:	case 0x3b /*⠃⠂⠇*/:
+	case 0x91 /*⠆⠂⠂*/:	case 0x93 /*⠇⠂⠂*/:	case 0x95 /*⠆⠃⠂*/:	case 0x99 /*⠆⠂⠃*/:	case 0x9b /*⠇⠂⠃*/:	case 0xb1 /*⠆⠂⠆*/:	case 0xb3 /*⠇⠂⠆*/:	case 0xb5 /*⠆⠃⠆*/:
+	case 0xb9 /*⠆⠂⠇*/:	case 0xbb /*⠇⠂⠇*/:
+		if (linePixels.get(x, y)) {
+			top = pixel;
+			bottom = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : pixel;
+		} else {
+			top = bottom = pixel;
+		}
+		break;
+	case 0x0a /*⠁⠂⠁*/:
+		top = pixel;
+		bottom = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		break;
+	case 0xa0 /*⠄⠂⠄*/:
+		top = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		bottom = pixel;
+		break;
+	case 0x16 /*⠁⠃⠂*/:	case 0x18 /*⠀⠂⠃*/:	case 0x1a /*⠁⠂⠃*/:	case 0x30 /*⠀⠂⠆*/:	case 0x32 /*⠁⠂⠆*/:	case 0x34 /*⠀⠃⠆*/:	case 0x36 /*⠁⠃⠆*/:	case 0x38 /*⠀⠂⠇*/:
+	case 0x3a /*⠁⠂⠇*/:
+		if (linePixels.get(x, y)) {
+			top = pixel;
+			bottom = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+		} else {
+			top = bottom = pixel;
+		}
+		break;
+	case 0x43 /*⠃⠆⠀*/:	case 0x61 /*⠂⠆⠄*/:
+		if (linePixels.get(x, y)) {
+			top = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+			bottom = pixel;
+		} else {
+			top = bottom = pixel;
+		}
+		break;
+	case 0x58 /*⠀⠆⠃*/:	case 0xd0 /*⠄⠆⠂*/:
+		if (linePixels.get(x, y)) {
+			top = !linePixels.get(x, y - 1) ? src.get(x, y - 1) : !linePixels.get(x - 1, y) ? src.get(x - 1, y) : pixel;
+			bottom = pixel;
+		} else {
+			top = bottom = pixel;
+		}
+		break;
+	case 0x03 /*⠃⠂⠀*/:	case 0x0b /*⠃⠂⠁*/:	case 0x0d /*⠂⠃⠁*/:	case 0x81 /*⠆⠂⠀*/:	case 0x83 /*⠇⠂⠀*/:	case 0x85 /*⠆⠃⠀*/:	case 0x89 /*⠆⠂⠁*/:	case 0x8b /*⠇⠂⠁*/:
+	case 0x8d /*⠆⠃⠁*/:
+		if (linePixels.get(x, y)) {
+			top = pixel;
+			bottom = !linePixels.get(x, y + 1) ? src.get(x, y + 1) : !linePixels.get(x + 1, y) ? src.get(x + 1, y) : pixel;
+		} else {
+			top = bottom = pixel;
+		}
+		break;
+	}
+}
diff --git a/graphics/larryScale_generator.js b/graphics/larryScale_generator.js
new file mode 100644
index 0000000..8df7b01
--- /dev/null
+++ b/graphics/larryScale_generator.js
@@ -0,0 +1,402 @@
+/* ScummVM - Graphic Adventure Engine
+*
+* ScummVM is the legal property of its developers, whose names
+* are too numerous to list here. Please refer to the COPYRIGHT
+* file distributed with this source distribution.
+*
+* 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 the Free Software Foundation; either version 2
+* of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*
+*/
+
+// This file re-generates 'larryScale_generated.cpp'.
+// To run it, install Node 8.0+, then run 'node larryScale_generator.js'.
+
+const fs = require('fs');
+
+// Compass directions
+const Direction = {
+	W:	0,
+	NW:	1,
+	N:	2,
+	NE:	3,
+	E:	4,
+	SE:	5,
+	S:	6,
+	SW:	7,
+
+	sanitize(direction) {
+		return ((direction % 8) + 8) % 8;
+	}
+};
+
+function getVector(direction) {
+	switch (direction) {
+	case Direction.W:	return [-1, 0];
+	case Direction.NW:	return [-1, -1];
+	case Direction.N:	return [0, -1];
+	case Direction.NE:	return [1, -1];
+	case Direction.E:	return [1, 0];
+	case Direction.SE:	return [1, 1];
+	case Direction.S:	return [0, 1];
+	case Direction.SW:	return [-1, 1];
+	default:
+		throw new Error(`Invalid direction: ${direction}`);
+	}
+}
+
+// An equality matrix is a combination of eight Boolean flags indicating whether
+// each of the surrounding pixels has the same color as the central pixel.
+//
+// +-----------+-----------+-----------+
+// | NW = 0x02 | N  = 0x04 | NE = 0x08 |
+// +-----------+-----------+-----------+
+// | W  = 0x01 | Reference | E  = 0x10 |
+// +-----------+-----------+-----------+
+// | SW = 0x80 | S  = 0x40 | SE = 0x20 |
+// +-----------+-----------+-----------+
+class EqualityMatrix {
+	constructor(value) {
+		this.value = value;
+	}
+
+	get(direction) {
+		const mask = 0x01 << Direction.sanitize(direction);
+		return (this.value & mask) != 0;
+	}
+
+	set(direction, flag) {
+		const mask = 0x01 << Direction.sanitize(direction);
+		this.value = this.value & ~mask | (flag ? mask : 0x00);
+	}
+
+	get w() { return this.get(Direction.W); }
+	set w(flag) { this.set(Direction.W, flag); }
+
+	get nw() { return this.get(Direction.NW); }
+	set nw(flag) { this.set(Direction.NW, flag); }
+
+	get n() { return this.get(Direction.N); }
+	set n(flag) { this.set(Direction.N, flag); }
+
+	get ne() { return this.get(Direction.NE); }
+	set ne(flag) { this.set(Direction.NE, flag); }
+
+	get e() { return this.get(Direction.E); }
+	set e(flag) { this.set(Direction.E, flag); }
+
+	get se() { return this.get(Direction.SE); }
+	set se(flag) { this.set(Direction.SE, flag); }
+
+	get s() { return this.get(Direction.S); }
+	set s(flag) { this.set(Direction.S, flag); }
+
+	get sw() { return this.get(Direction.SW); }
+	set sw(flag) { this.set(Direction.SW, flag); }
+
+	toBraille() {
+		return getBrailleColumn(this.nw, this.w, this.sw)
+			+ getBrailleColumn(this.n, true, this.s)
+			+ getBrailleColumn(this.ne, this.e, this.se);
+	}
+}
+
+function getBrailleColumn(top, middle, bottom) {
+	const codepoint = 0x2800 | (top ? 1 : 0) | (middle ? 2 : 0) | (bottom ? 4 : 0);
+	return String.fromCodePoint(codepoint);
+}
+
+function indent(string, tabCount = 1) {
+	const indentation = '\t'.repeat(tabCount);
+	return string
+		.split(/\r?\n/)
+		.map(s => indentation + s)
+		.join('\n');
+}
+
+function toHex(number, minLength = 2) {
+	const hex = number.toString(16);
+	const padding = '0'.repeat(Math.max(minLength - hex.length, 0));
+	return `0x${padding}${hex}`;
+}
+
+function generateCaseLabel(matrix) {
+	return `case ${toHex(matrix.value)} /*${matrix.toBraille()}*/:`
+}
+
+function generateCaseBlock(matrixes, body) {
+	const maxLabelsPerLine = 8;
+	const labels = matrixes
+		.map(generateCaseLabel)
+		.reduce((a, b, index) => a + ((index % maxLabelsPerLine === 0) ? '\n' : '\t') + b);
+	return `${labels}\n${indent(body)}`;
+}
+
+function generateSwitchBlock(variableName, getCaseBody) {
+	const matrixesByBody = new Map();
+	for (let value = 0; value <= 0xFF; value++) {
+		const matrix = new EqualityMatrix(value);
+		const body = getCaseBody(matrix);
+		if (!matrixesByBody.has(body)) {
+			matrixesByBody.set(body, []);
+		}
+		matrixesByBody.get(body).push(matrix);
+	}
+	const orderedPairs = [...matrixesByBody.entries()]
+		// For readability: order cases by increasing code length
+		.sort((a, b) => a[0].length - b[0].length);
+	const switchStatements = orderedPairs
+		.map(([body, matrixes]) => generateCaseBlock(matrixes, body))
+		.join('\n');
+	const comment = '// Note: There is a case label for every possible value, so we don\'t need a default label.';
+	return `${comment}\nswitch (${variableName}) {\n${switchStatements}\n}`;
+}
+
+const PixelType = {
+	// Pixel is part of a line
+	LINE: 'line',
+	// Pixel is part of a fill
+	FILL: 'fill',
+	// Pixel is part of a line *or* a fill
+	INDETERMINATE: 'indeterminate'
+};
+
+function getPixelType(matrix) {
+	// Single pixels are fills
+	if (matrix.value === 0) return PixelType.FILL;
+
+	// 2x2 blocks are fills
+	if (
+		(matrix.n && matrix.ne && matrix.e)
+		|| (matrix.e && matrix.se && matrix.s)
+		|| (matrix.s && matrix.sw && matrix.w)
+		|| (matrix.w && matrix.nw && matrix.n)
+	) return PixelType.FILL;
+
+	// A pixel adjacent to a 2x2 block is a fill.
+	// This requires reading out of the matrix, so we can't be sure.
+	if (
+		(matrix.n && matrix.ne)
+		|| (matrix.ne && matrix.e)
+		|| (matrix.e && matrix.se)
+		|| (matrix.se && matrix.s)
+		|| (matrix.s && matrix.sw)
+		|| (matrix.sw && matrix.w)
+		|| (matrix.w && matrix.nw)
+		|| (matrix.nw && matrix.n)
+	) return PixelType.INDETERMINATE;
+
+	// Everything else is part of a line
+	return PixelType.LINE;
+}
+
+function isPowerOfTwo(number) {
+    return Math.log2(number) % 1 === 0;
+}
+
+// Upscales a line pixel to 2x2.
+// Returns a 4-element array of Booleans in order top-left, top-right, bottom-left, bottom-right.
+// Each Boolean indicates whether the upscaled pixel should be filled with the original color.
+function getLineUpscaleFlags(matrix) {
+	// The rules for upscaling lines are *not* symmetrical but biased toward the left
+
+	// Special rules for upscaling smooth angled lines
+	switch (matrix.value) {
+	case 0x34 /*⠀⠃⠆*/:
+		return [false, true, false, false];	// [ ▀]
+	case 0x58 /*⠀⠆⠃*/:
+		return [false, false, false, true];	// [ ▄]
+	case 0x43 /*⠃⠆⠀*/:
+		return [false, false, true, false];	// [▄ ]
+
+	case 0x61 /*⠂⠆⠄*/:
+		return [false, false, true, false];	// [▄ ]
+	case 0x16 /*⠁⠃⠂*/:
+		return [false, true, false, false];	// [ ▀]
+	case 0xD0 /*⠄⠆⠂*/:
+		return [false, false, false, true];	// [ ▄]
+
+	case 0x24 /*⠀⠃⠄*/:
+	case 0x48 /*⠀⠆⠁*/:
+		return [false, true, false, true];	// [ █]
+
+	case 0x21 /*⠂⠂⠄*/:
+	case 0x90 /*⠄⠂⠂*/:
+		return [false, false, true, true];	// [▄▄]
+
+	case 0x50 /*⠀⠆⠂*/:
+		return [true, true, true, false];	// [█▀]
+	}
+
+	// Generic rules for upscaling lines
+
+	// Ignore diagonals next to fully-adjacent pixels
+	matrix = new EqualityMatrix(matrix.value);
+	if (matrix.w) {
+		matrix.sw = matrix.nw = false;
+	}
+	if (matrix.n) {
+		matrix.nw = matrix.ne = false;
+	}
+	if (matrix.e) {
+		matrix.ne = matrix.se = false;
+	}
+	if (matrix.s) {
+		matrix.se = matrix.sw = false;
+	}
+
+	// Mirror single lines
+	if (isPowerOfTwo(matrix.value)) {
+		matrix.value |= (matrix.value << 4) | (matrix.value >> 4);
+	}
+
+	return [
+		matrix.w || matrix.nw || matrix.n,
+		matrix.ne || matrix.e,
+		matrix.s || matrix.sw,
+		matrix.se
+	];
+}
+
+// Upscales a fill pixel to 2x2.
+// Same result format as getLineUpscaleFlags.
+function getFillUpscaleFlags(matrix) {
+	// The rules for upscaling fills are *not* symmetrical but biased toward the top-left
+
+	// Special rules for upscaling cornered fills
+	switch (matrix.value) {
+	case 0xE1 /*⠆⠆⠄*/:
+		return [false, false, true, true];	// [▄▄]
+	case 0x0F /*⠃⠃⠁*/:
+		return [true, true, false, false];	// [▀▀]
+	case 0xC3 /*⠇⠆⠀*/:
+	case 0x87 /*⠇⠃⠀*/:
+		return [true, false, true, false];	// [█ ]
+	}
+
+	// Generic rules for upscaling fills
+	if (!matrix.s && !matrix.se && !matrix.e && (matrix.sw || matrix.ne)) {
+		return [true, true, true, false];	// [█▀]
+	} else if (!matrix.n && !matrix.ne && !matrix.e && (matrix.nw || matrix.se)) {
+		return [true, false, true, true];	// [█▄]
+	} else {
+		return [true, true, true, true];	// [██]
+	}
+}
+
+function formatOffset(number) {
+	if (number < 0) {
+		return ` - ${-number}`;
+	}
+	if (number > 0) {
+		return ` + ${number}`;
+	}
+	return '';
+}
+
+function generatePixelUpscaleCode(matrix, flags, pixelRecords, { generateBreak = true } = {}) {
+	const targetsByValue = new Map();
+	function addAssignment(param, value) {
+		if (targetsByValue.has(value)) {
+			targetsByValue.get(value).push(param);
+		} else {
+			targetsByValue.set(value, [param]);
+		}
+	}
+	for (const pixelRecord of pixelRecords) {
+		const param = pixelRecord.param;
+		const useSourceColor = flags
+			.filter((flag, index) => pixelRecord.flagIndexes.includes(index))
+			.some(flag => flag);
+		if (useSourceColor) {
+			addAssignment(param, 'pixel');
+		} else {
+			const sourceDirections = pixelRecord.sourceDirections
+				.filter(d => !matrix.get(d));
+			const value = sourceDirections
+				.filter(d => !matrix.get(d)) // We don't want to get our own color
+				.map(d => {
+					const vector = getVector(d);
+					const otherValueCode = `src.get(x${formatOffset(vector[0])}, y${formatOffset(vector[1])})`;
+					return `!linePixels.get(x${formatOffset(vector[0])}, y${formatOffset(vector[1])}) ? ${otherValueCode} : `;
+				})
+				.join('') + 'pixel';
+			addAssignment(param, value);
+		}
+	}
+
+	return [...targetsByValue.entries()]
+		.map(([value, targets]) => [...targets, value].join(' = ') + ';')
+		.concat(generateBreak ? ['break;'] : [])
+		.join('\n');
+}
+
+function generateScalePixelFunction(width, height, pixelRecords) {
+	const params = pixelRecords
+		.map((pixelRecord, index) => `Color &${pixelRecord.param}`)
+		.join(', ');
+	const header =
+		`inline void scalePixelTo${width}x${height}(\n\tconst MarginedBitmap<Color> &src,\n\tconst MarginedBitmap<bool> &linePixels,\n\tint x, int y,\n\t// Out parameters\n\t${params}\n)`;
+	const prefix =
+		'const Color pixel = src.get(x, y);\n'
+		+ 'const EqualityMatrix matrix = getEqualityMatrix(src.getPointerTo(x, y), src.getStride());';
+	const switchBlock = generateSwitchBlock('matrix', matrix => {
+		const pixelType = getPixelType(matrix);
+		switch (pixelType) {
+		case PixelType.LINE:
+			return generatePixelUpscaleCode(matrix, getLineUpscaleFlags(matrix), pixelRecords);
+		case PixelType.FILL:
+			return generatePixelUpscaleCode(matrix, getFillUpscaleFlags(matrix), pixelRecords);
+		case PixelType.INDETERMINATE:
+			const lineUpscaleCode = generatePixelUpscaleCode(matrix, getLineUpscaleFlags(matrix), pixelRecords, { generateBreak: false });
+			const fillUpscaleCode = generatePixelUpscaleCode(matrix, getFillUpscaleFlags(matrix), pixelRecords, { generateBreak: false });
+			return `if (linePixels.get(x, y)) {\n${indent(lineUpscaleCode)}\n} else {\n${indent(fillUpscaleCode)}\n}\nbreak;`;
+		}
+	});
+	return `${header} {\n${indent(prefix)}\n\n${indent(switchBlock)}\n}`;
+}
+
+function generateScalePixelTo2x2() {
+	const pixelRecords = [
+		{ param: 'topLeft',		flagIndexes: [0], sourceDirections: [Direction.N, Direction.W] },
+		{ param: 'topRight',	flagIndexes: [1], sourceDirections: [Direction.N, Direction.E] },
+		{ param: 'bottomLeft',	flagIndexes: [2], sourceDirections: [Direction.S, Direction.W] },
+		{ param: 'bottomRight',	flagIndexes: [3], sourceDirections: [Direction.S, Direction.E] }
+	];
+	return generateScalePixelFunction(2, 2, pixelRecords);
+}
+
+function generateScalePixelTo2x1() {
+	const pixelRecords = [
+		{ param: 'left',	flagIndexes: [0, 2], sourceDirections: [Direction.N, Direction.W, Direction.S] },
+		{ param: 'right',	flagIndexes: [1, 3], sourceDirections: [Direction.N, Direction.E, Direction.S] }
+	];
+	return generateScalePixelFunction(2, 1, pixelRecords);
+}
+
+function generateScalePixelTo1x2() {
+	const pixelRecords = [
+		{ param: 'top',		flagIndexes: [0, 1], sourceDirections: [Direction.N, Direction.W, Direction.E] },
+		{ param: 'bottom',	flagIndexes: [2, 3], sourceDirections: [Direction.S, Direction.W, Direction.E] }
+	];
+	return generateScalePixelFunction(1, 2, pixelRecords);
+}
+
+const generators = [generateScalePixelTo2x2, generateScalePixelTo2x1, generateScalePixelTo1x2];
+const generatedFunctions = generators
+	.map(generator => generator())
+	.join('\n\n');
+const legalese = fs.readFileSync(__filename, 'utf8').match(/\/\*[\s\S]*?\*\//)[0];
+const headerComment = '// This file was generated by larryScale_generator.js.\n// Do not edit directly! Instead, edit the generator script and run it.'
+fs.writeFileSync('./larryScale_generated.cpp', `${legalese}\n\n${headerComment}\n\n${generatedFunctions}\n`);
diff --git a/graphics/module.mk b/graphics/module.mk
index dbe5a3b..6076768 100644
--- a/graphics/module.mk
+++ b/graphics/module.mk
@@ -12,6 +12,7 @@ MODULE_OBJS := \
 	fonts/newfont.o \
 	fonts/ttf.o \
 	fonts/winfont.o \
+	larryScale.o \
 	maccursor.o \
 	macgui/macfontmanager.o \
 	macgui/macmenu.o \


Commit: f8c32b07e7bc219e81df6d1f27209ac28d3ced6b
    https://github.com/scummvm/scummvm/commit/f8c32b07e7bc219e81df6d1f27209ac28d3ced6b
Author: Daniel Wolf (dwolf at dannad.de)
Date: 2018-03-16T22:33:06+01:00

Commit Message:
SCI: Use LarryScale cel scaler instead of nearest-neighbor

Changed paths:
    engines/sci/graphics/celobj32.cpp


diff --git a/engines/sci/graphics/celobj32.cpp b/engines/sci/graphics/celobj32.cpp
index fb7abe8..fbf8f81 100644
--- a/engines/sci/graphics/celobj32.cpp
+++ b/engines/sci/graphics/celobj32.cpp
@@ -31,6 +31,7 @@
 #include "sci/graphics/text32.h"
 #include "sci/engine/workarounds.h"
 #include "sci/util.h"
+#include "graphics/larryScale.h"
 
 namespace Sci {
 #pragma mark CelScaler
@@ -154,6 +155,9 @@ struct SCALER_Scale {
 #endif
 	const byte *_row;
 	READER _reader;
+	// If _sourceBuffer is set, it contains the full (possibly scaled) source
+	// image and takes precedence over _reader.
+	Common::SharedPtr<Buffer> _sourceBuffer;
 	int16 _x;
 	static int16 _valuesX[kCelScalerTableSize];
 	static int16 _valuesY[kCelScalerTableSize];
@@ -167,7 +171,8 @@ struct SCALER_Scale {
 	// The maximum width of the scaled object may not be as wide as the source
 	// data it requires if downscaling, so just always make the reader
 	// decompress an entire line of source data when scaling
-	_reader(celObj, celObj._width) {
+	_reader(celObj, celObj._width),
+	_sourceBuffer() {
 #ifndef NDEBUG
 		assert(_minX <= _maxX);
 #endif
@@ -196,43 +201,98 @@ struct SCALER_Scale {
 
 		const CelScalerTable &table = CelObj::_scaler->getScalerTable(scaleX, scaleY);
 
-		if (g_sci->_gfxFrameout->getScriptWidth() == kLowResX) {
-			const int16 unscaledX = (scaledPosition.x / scaleX).toInt();
-			if (FLIP) {
-				const int lastIndex = celObj._width - 1;
-				for (int16 x = targetRect.left; x < targetRect.right; ++x) {
-					_valuesX[x] = lastIndex - (table.valuesX[x] - unscaledX);
+		const bool useLarryScale = true;
+		if (useLarryScale) {
+			// LarryScale is an alternative, high-quality cel scaler implemented
+			// for ScummVM. Due to the nature of smooth upscaling, it does *not*
+			// respect the global scaling pattern. Instead, it simply scales the
+			// cel to the extent of targetRect.
+
+			class Copier: public Graphics::RowReader, public Graphics::RowWriter {
+				READER &_souceReader;
+				Buffer &_targetBuffer;
+			public:
+				Copier(READER& souceReader, Buffer& targetBuffer) :
+					_souceReader(souceReader),
+					_targetBuffer(targetBuffer) {}
+				const Graphics::LarryScaleColor* readRow(int y) {
+					return _souceReader.getRow(y);
 				}
-			} else {
-				for (int16 x = targetRect.left; x < targetRect.right; ++x) {
-					_valuesX[x] = table.valuesX[x] - unscaledX;
+				void writeRow(int y, const Graphics::LarryScaleColor* row) {
+					memcpy(_targetBuffer.getBasePtr(0, y), row, _targetBuffer.w);
 				}
+			};
+
+			// Scale the cel using LarryScale and write it to _sourceBuffer
+			// scaledImageRect is not necessarily identical to targetRect
+			// because targetRect may be cropped to render only a segment.
+			Common::Rect scaledImageRect(
+				scaledPosition.x,
+				scaledPosition.y,
+				scaledPosition.x + (celObj._width * scaleX).toInt(),
+				scaledPosition.y + (celObj._height * scaleY).toInt());
+			_sourceBuffer = Common::SharedPtr<Buffer>(new Buffer(), Graphics::SurfaceDeleter());
+			_sourceBuffer->create(
+				scaledImageRect.width(), scaledImageRect.height(),
+				Graphics::PixelFormat::createFormatCLUT8());
+			Copier copier(_reader, *_sourceBuffer);
+			Graphics::larryScale(
+				celObj._width, celObj._height, celObj._skipColor, copier,
+				scaledImageRect.width(), scaledImageRect.height(), copier);
+
+			// Set _valuesX and _valuesY to reference the scaled image without additional scaling
+			for (int16 x = targetRect.left; x < targetRect.right; ++x) {
+				const int16 unsafeValue = FLIP
+					? scaledImageRect.right - x - 1
+					: x - scaledImageRect.left;
+				_valuesX[x] = CLIP<int16>(unsafeValue, 0, scaledImageRect.width() - 1);
 			}
-
-			const int16 unscaledY = (scaledPosition.y / scaleY).toInt();
 			for (int16 y = targetRect.top; y < targetRect.bottom; ++y) {
-				_valuesY[y] = table.valuesY[y] - unscaledY;
+				const int16 unsafeValue = y - scaledImageRect.top;
+				_valuesY[y] = CLIP<int16>(unsafeValue, 0, scaledImageRect.height() - 1);
 			}
 		} else {
-			if (FLIP) {
-				const int lastIndex = celObj._width - 1;
-				for (int16 x = targetRect.left; x < targetRect.right; ++x) {
-					_valuesX[x] = lastIndex - table.valuesX[x - scaledPosition.x];
+			const bool useGlobalScaling = g_sci->_gfxFrameout->getScriptWidth() == kLowResX;
+			if (useGlobalScaling) {
+				const int16 unscaledX = (scaledPosition.x / scaleX).toInt();
+				if (FLIP) {
+					const int lastIndex = celObj._width - 1;
+					for (int16 x = targetRect.left; x < targetRect.right; ++x) {
+						_valuesX[x] = lastIndex - (table.valuesX[x] - unscaledX);
+					}
+				} else {
+					for (int16 x = targetRect.left; x < targetRect.right; ++x) {
+						_valuesX[x] = table.valuesX[x] - unscaledX;
+					}
+				}
+
+				const int16 unscaledY = (scaledPosition.y / scaleY).toInt();
+				for (int16 y = targetRect.top; y < targetRect.bottom; ++y) {
+					_valuesY[y] = table.valuesY[y] - unscaledY;
 				}
 			} else {
-				for (int16 x = targetRect.left; x < targetRect.right; ++x) {
-					_valuesX[x] = table.valuesX[x - scaledPosition.x];
+				if (FLIP) {
+					const int lastIndex = celObj._width - 1;
+					for (int16 x = targetRect.left; x < targetRect.right; ++x) {
+						_valuesX[x] = lastIndex - table.valuesX[x - scaledPosition.x];
+					}
+				} else {
+					for (int16 x = targetRect.left; x < targetRect.right; ++x) {
+						_valuesX[x] = table.valuesX[x - scaledPosition.x];
+					}
 				}
-			}
 
-			for (int16 y = targetRect.top; y < targetRect.bottom; ++y) {
-				_valuesY[y] = table.valuesY[y - scaledPosition.y];
+				for (int16 y = targetRect.top; y < targetRect.bottom; ++y) {
+					_valuesY[y] = table.valuesY[y - scaledPosition.y];
+				}
 			}
 		}
 	}
 
 	inline void setTarget(const int16 x, const int16 y) {
-		_row = _reader.getRow(_valuesY[y]);
+		_row = _sourceBuffer
+			? static_cast<const byte *>( _sourceBuffer->getBasePtr(0, _valuesY[y]))
+			: _reader.getRow(_valuesY[y]);
 		_x = x;
 		assert(_x >= _minX && _x <= _maxX);
 	}


Commit: 999cf71dcf4ac9466915edd55a9c91094db1a99c
    https://github.com/scummvm/scummvm/commit/999cf71dcf4ac9466915edd55a9c91094db1a99c
Author: Daniel Wolf (dwolf at dannad.de)
Date: 2018-03-16T22:33:06+01:00

Commit Message:
SCI: Add game option for enabling/disabling LarryScale in LSL7

All other SCI games continue using the default scaler.

Changed paths:
    common/gui_options.cpp
    common/gui_options.h
    engines/sci/detection.cpp
    engines/sci/detection_tables.h
    engines/sci/graphics/celobj32.cpp
    engines/sci/sci.h


diff --git a/common/gui_options.cpp b/common/gui_options.cpp
index 18e02c1..6f340bf 100644
--- a/common/gui_options.cpp
+++ b/common/gui_options.cpp
@@ -84,6 +84,7 @@ const struct GameOpt {
 	// "gameOption10" would be invalid here because it contains "gameOption1"
 	{ GUIO_GAMEOPTIONS10, "gameOptionA" },
 	{ GUIO_GAMEOPTIONS11, "gameOptionB" },
+	{ GUIO_GAMEOPTIONS12, "gameOptionC" },
 
 	{ GUIO_NONE, 0 }
 };
diff --git a/common/gui_options.h b/common/gui_options.h
index 0813ff1..14d4cb9 100644
--- a/common/gui_options.h
+++ b/common/gui_options.h
@@ -76,6 +76,7 @@
 #define GUIO_GAMEOPTIONS9    "\060"
 #define GUIO_GAMEOPTIONS10   "\061"
 #define GUIO_GAMEOPTIONS11   "\062"
+#define GUIO_GAMEOPTIONS12   "\063"
 
 #define GUIO0() (GUIO_NONE)
 #define GUIO1(a) (a)
diff --git a/engines/sci/detection.cpp b/engines/sci/detection.cpp
index aa7a63d..36496e8 100644
--- a/engines/sci/detection.cpp
+++ b/engines/sci/detection.cpp
@@ -437,6 +437,16 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 #endif
 
 	{
+		GAMEOPTION_LARRYSCALE,
+		{
+			_s("Use high-quality \"LarryScale\" cel scaling"),
+			_s("Use special cartoon scaler for drawing character sprites"),
+			"enable_larryscale",
+			true
+		}
+	},
+
+	{
 		GAMEOPTION_PREFER_DIGITAL_SFX,
 		{
 			_s("Prefer digital sound effects"),
diff --git a/engines/sci/detection_tables.h b/engines/sci/detection_tables.h
index e9fba20..b908d5b 100644
--- a/engines/sci/detection_tables.h
+++ b/engines/sci/detection_tables.h
@@ -2733,11 +2733,12 @@ static const struct ADGameDescription SciGameDescriptions[] = {
 #define GUIO_LSL7_DEMO GUIO3(GUIO_NOASPECT, \
                              GUIO_NOMIDI, \
                              GUIO_NOLAUNCHLOAD)
-#define GUIO_LSL7      GUIO5(GUIO_NOASPECT, \
+#define GUIO_LSL7      GUIO6(GUIO_NOASPECT, \
                              GUIO_NOMIDI, \
                              GAMEOPTION_ENABLE_BLACK_LINED_VIDEO, \
                              GAMEOPTION_ORIGINAL_SAVELOAD, \
-                             GAMEOPTION_HQ_VIDEO)
+                             GAMEOPTION_HQ_VIDEO, \
+                             GAMEOPTION_LARRYSCALE)
 
 	// Larry 7 - English DOS Demo (provided by richiefs in bug report #2670691)
 	// SCI interpreter version 2.100.002
diff --git a/engines/sci/graphics/celobj32.cpp b/engines/sci/graphics/celobj32.cpp
index fbf8f81..f11c75a 100644
--- a/engines/sci/graphics/celobj32.cpp
+++ b/engines/sci/graphics/celobj32.cpp
@@ -32,6 +32,7 @@
 #include "sci/engine/workarounds.h"
 #include "sci/util.h"
 #include "graphics/larryScale.h"
+#include "common/config-manager.h"
 
 namespace Sci {
 #pragma mark CelScaler
@@ -201,7 +202,7 @@ struct SCALER_Scale {
 
 		const CelScalerTable &table = CelObj::_scaler->getScalerTable(scaleX, scaleY);
 
-		const bool useLarryScale = true;
+		const bool useLarryScale = ConfMan.getBool("enable_larryscale");
 		if (useLarryScale) {
 			// LarryScale is an alternative, high-quality cel scaler implemented
 			// for ScummVM. Due to the nature of smooth upscaling, it does *not*
diff --git a/engines/sci/sci.h b/engines/sci/sci.h
index faa1532..6245d18 100644
--- a/engines/sci/sci.h
+++ b/engines/sci/sci.h
@@ -58,6 +58,7 @@ namespace Sci {
 #define GAMEOPTION_ENABLE_BLACK_LINED_VIDEO GUIO_GAMEOPTIONS9
 #define GAMEOPTION_HQ_VIDEO                 GUIO_GAMEOPTIONS10
 #define GAMEOPTION_ENABLE_CENSORING         GUIO_GAMEOPTIONS11
+#define GAMEOPTION_LARRYSCALE               GUIO_GAMEOPTIONS12
 
 struct EngineState;
 class Vocabulary;


Commit: 4229e2642ef33f761528cc5811726df56f3a42d0
    https://github.com/scummvm/scummvm/commit/4229e2642ef33f761528cc5811726df56f3a42d0
Author: Thierry Crozat (criezy at scummvm.org)
Date: 2018-06-03T17:41:47+01:00

Commit Message:
Merge pull request #1141 from DanielSWolf/larryscale

SCI: High-quality "LarryScale" cel scaler for LSL7

Changed paths:
  A graphics/larryScale.cpp
  A graphics/larryScale.h
  A graphics/larryScale_generated.cpp
  A graphics/larryScale_generator.js
    common/gui_options.cpp
    common/gui_options.h
    engines/sci/detection.cpp
    engines/sci/detection_tables.h
    engines/sci/graphics/celobj32.cpp
    engines/sci/sci.h
    graphics/module.mk


diff --cc common/gui_options.cpp
index 00de6ec,6f340bf..06b6c20
--- a/common/gui_options.cpp
+++ b/common/gui_options.cpp
@@@ -84,8 -84,9 +84,9 @@@ const struct GameOpt 
  	// "gameOption10" would be invalid here because it contains "gameOption1"
  	{ GUIO_GAMEOPTIONS10, "gameOptionA" },
  	{ GUIO_GAMEOPTIONS11, "gameOptionB" },
+ 	{ GUIO_GAMEOPTIONS12, "gameOptionC" },
  
 -	{ GUIO_NONE, 0 }
 +	{ GUIO_NONE, nullptr }
  };
  
  bool checkGameGUIOption(const String &option, const String &str) {





More information about the Scummvm-git-logs mailing list