/* 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 #include "common/array.h" #include 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 class MarginedBitmap { int _width; int _height; int _stride; Common::Array _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 void MarginedBitmap::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 createMarginedBitmap(int width, int height, Color marginColor, RowReader &rowReader) { MarginedBitmap 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 &_target; public: explicit MarginedBitmapWriter(MarginedBitmap &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 &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 createMarginedLinePixelsBitmap(const MarginedBitmap &src) { MarginedBitmap 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 &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 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 &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 linePixels = createMarginedLinePixelsBitmap(src); Common::Array topDstRow(dstWidth); Common::Array 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 &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 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 src = createMarginedBitmap(srcWidth, srcHeight, transparentColor, rowReader); larryScale(src, transparentColor, dstWidth, dstHeight, rowWriter); } } }