aboutsummaryrefslogtreecommitdiff
path: root/graphics/larryScale.cpp
diff options
context:
space:
mode:
authorDaniel Wolf2018-03-15 15:34:59 +0100
committerDaniel Wolf2018-03-16 22:33:06 +0100
commitaf0434cf091c868ad55ce8885872a147fbd22262 (patch)
tree0681b4bc201be18313ccd03a4cea049ffa9674c5 /graphics/larryScale.cpp
parentfb882a10ae4db644b31782ee5a2f6b7ba6ae651e (diff)
downloadscummvm-rg350-af0434cf091c868ad55ce8885872a147fbd22262.tar.gz
scummvm-rg350-af0434cf091c868ad55ce8885872a147fbd22262.tar.bz2
scummvm-rg350-af0434cf091c868ad55ce8885872a147fbd22262.zip
GRAPHICS: Implement LarryScale cel scaling algorithm
Diffstat (limited to 'graphics/larryScale.cpp')
-rw-r--r--graphics/larryScale.cpp406
1 files changed, 406 insertions, 0 deletions
diff --git a/graphics/larryScale.cpp b/graphics/larryScale.cpp
new file mode 100644
index 0000000000..c4b2f6e88e
--- /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);
+ }
+}
+
+}