mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-15 14:18:37 +00:00
404 lines
12 KiB
C++
404 lines
12 KiB
C++
/* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "larryScale.h"
|
|
#include "common/array.h"
|
|
|
|
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) override {
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|