[Scummvm-git-logs] scummvm master -> 36cfb5f6f932fb5ac259a43eb1c8256531e7d1ae

neuromancer noreply at scummvm.org
Sat Jun 13 06:14:05 UTC 2026


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

Summary:
36cfb5f6f9 PRIVATE: added option to enhance contrast of reading material


Commit: 36cfb5f6f932fb5ac259a43eb1c8256531e7d1ae
    https://github.com/scummvm/scummvm/commit/36cfb5f6f932fb5ac259a43eb1c8256531e7d1ae
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-13T08:10:25+02:00

Commit Message:
PRIVATE: added option to enhance contrast of reading material

Changed paths:
  A engines/private/paper.cpp
  A engines/private/paper.h
    engines/private/detection.cpp
    engines/private/detection.h
    engines/private/metaengine.cpp
    engines/private/module.mk
    engines/private/private.cpp
    engines/private/private.h


diff --git a/engines/private/detection.cpp b/engines/private/detection.cpp
index 29334ed0885..6650c63afd5 100644
--- a/engines/private/detection.cpp
+++ b/engines/private/detection.cpp
@@ -48,7 +48,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Demo from the US release v1.0.0.23
@@ -58,7 +58,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformWindows,
 		ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye",  // EU release (UK)
@@ -68,7 +68,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_GRB,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Demo from the EU release
@@ -78,7 +78,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_GRB,
 		Common::kPlatformWindows,
 		ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Demo from PCGamer Disc 2.6 JULY 1996 v1.0.0.12
@@ -88,7 +88,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformWindows,
 		ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Another demo
@@ -98,7 +98,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformWindows,
 		ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // EU release (ES)
@@ -108,7 +108,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::ES_ESP,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Demo from the EU release (ES)
@@ -118,7 +118,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::ES_ESP,
 		Common::kPlatformWindows,
 		ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // EU release (FR)
@@ -128,7 +128,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::FR_FRA,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // EU release (DE)
@@ -138,7 +138,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::DE_DEU,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Promotional demo disc
@@ -148,7 +148,7 @@ static const ADGameDescription gameDescriptions[] = {
 			Common::EN_USA,
 			Common::kPlatformWindows,
 			ADGF_DEMO,
-			GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+			GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Demo from the EU release (DE)
@@ -158,7 +158,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::DE_DEU,
 		Common::kPlatformWindows,
 		ADGF_DEMO,
-		GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // Demo from the EU release (FR)
@@ -168,7 +168,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::FR_FRA,
 		Common::kPlatformWindows,
 		ADGF_DEMO,
-		GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye",  // RU release
@@ -178,7 +178,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::RU_RUS,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye",  // KO release
@@ -188,7 +188,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::KO_KOR,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye",  // JP release
@@ -198,7 +198,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::JA_JPN,
 		Common::kPlatformWindows,
 		ADGF_NO_FLAGS,
-		GUIO2(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO3(GUIO_NOMIDI, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // MacOS release (US)
@@ -208,7 +208,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformMacintosh,
 		ADGF_NO_FLAGS,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // MacOS release (US) uninstalled
@@ -217,7 +217,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformMacintosh,
 		ADGF_NO_FLAGS,
-		GUIO1(GUIO_NOMIDI)
+		GUIO2(GUIO_NOMIDI, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // MacOS release (JP) uninstalled
@@ -226,7 +226,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::JA_JPN,
 		Common::kPlatformMacintosh,
 		ADGF_NO_FLAGS,
-		GUIO1(GUIO_NOMIDI)
+		GUIO2(GUIO_NOMIDI, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // MacOS demo (US)
@@ -236,7 +236,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformMacintosh,
 		ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_SFX_SUBTITLES, GAMEOPTION_HIGHLIGHT_MASKS, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	{
 		"private-eye", // MacOS demo (US) uninstalled
@@ -245,7 +245,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_USA,
 		Common::kPlatformMacintosh,
 		ADGF_DEMO,
-		GUIO1(GUIO_NOMIDI)
+		GUIO2(GUIO_NOMIDI, GAMEOPTION_READING_MATERIAL_CONTRAST)
 	},
 	AD_TABLE_END_MARKER
 };
diff --git a/engines/private/detection.h b/engines/private/detection.h
index 7e0d803276d..42e899394ab 100644
--- a/engines/private/detection.h
+++ b/engines/private/detection.h
@@ -3,5 +3,6 @@
 
 #define GAMEOPTION_SFX_SUBTITLES GUIO_GAMEOPTIONS1
 #define GAMEOPTION_HIGHLIGHT_MASKS GUIO_GAMEOPTIONS2
+#define GAMEOPTION_READING_MATERIAL_CONTRAST GUIO_GAMEOPTIONS3
 
 #endif // PRIVATE_DETECTION_H
diff --git a/engines/private/metaengine.cpp b/engines/private/metaengine.cpp
index bcf2aa24882..bdcab09986f 100644
--- a/engines/private/metaengine.cpp
+++ b/engines/private/metaengine.cpp
@@ -55,6 +55,17 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+	{
+		GAMEOPTION_READING_MATERIAL_CONTRAST,
+		{
+			_s("Increased contrast for small print"),
+			_s("Enhance small printed paper close-ups for improved readability."),
+			"readingMaterialContrast",
+			false,
+			0,
+			0
+		}
+	},
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
diff --git a/engines/private/module.mk b/engines/private/module.mk
index 44de2aabf26..300b695fbd5 100644
--- a/engines/private/module.mk
+++ b/engines/private/module.mk
@@ -8,6 +8,7 @@ MODULE_OBJS := \
 	grammar.o \
 	lexer.o \
 	metaengine.o \
+	paper.o \
 	private.o \
 	savegame.o \
 	symbol.o
diff --git a/engines/private/paper.cpp b/engines/private/paper.cpp
new file mode 100644
index 00000000000..cb7776cc896
--- /dev/null
+++ b/engines/private/paper.cpp
@@ -0,0 +1,274 @@
+/* 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 "common/array.h"
+#include "common/path.h"
+#include "common/str.h"
+#include "common/util.h"
+#include "graphics/palette.h"
+#include "graphics/surface.h"
+
+#include "private/paper.h"
+
+namespace Private {
+
+const int kPaperScanTransparentBackgroundLuma = 180;
+
+// Nearest palette color in YCbCr space among the allowed entries. Chroma
+// errors are weighted heavier than luminance ones: a wrong brightness just
+// looks like paper grain, while a wrong hue stands out as colored speckle.
+byte findNearestUsedColor(const byte *luma, const byte *cb, const byte *cr,
+		const Common::Array<byte> &allowedColors, int targetY, int targetCb, int targetCr) {
+	byte bestColor = allowedColors[0];
+	int bestDistance = 0x7fffffff;
+
+	for (uint i = 0; i < allowedColors.size(); i++) {
+		byte color = allowedColors[i];
+		int dy = luma[color] - targetY;
+		int dcb = cb[color] - targetCb;
+		int dcr = cr[color] - targetCr;
+		int distance = 2 * dy * dy + 5 * (dcb * dcb + dcr * dcr);
+
+		if (distance < bestDistance) {
+			bestDistance = distance;
+			bestColor = color;
+		}
+	}
+
+	return bestColor;
+}
+
+void blurPlane(const Graphics::Surface &image, byte transparentColor, const byte *src, byte *dst, byte *tmp) {
+	const int kernel[5] = {1, 14, 34, 14, 1};
+	const int width = image.w;
+	const int height = image.h;
+
+	for (int y = 0; y < height; y++) {
+		const byte *row = (const byte *)image.getBasePtr(0, y);
+		const byte *srcRow = src + y * width;
+		byte *tmpRow = tmp + y * width;
+		for (int x = 0; x < width; x++) {
+			int center = srcRow[x];
+			int sum = 0;
+			for (int k = -2; k <= 2; k++) {
+				int xx = x + k;
+				bool outside = xx < 0 || xx >= width || row[xx] == transparentColor;
+				sum += kernel[k + 2] * (outside ? center : srcRow[xx]);
+			}
+			tmpRow[x] = (sum + 32) >> 6;
+		}
+	}
+
+	for (int y = 0; y < height; y++) {
+		const byte *tmpRow = tmp + y * width;
+		byte *dstRow = dst + y * width;
+		for (int x = 0; x < width; x++) {
+			int center = tmpRow[x];
+			int sum = 0;
+			for (int k = -2; k <= 2; k++) {
+				int yy = y + k;
+				bool outside = yy < 0 || yy >= height;
+				if (!outside) {
+					const byte *row = (const byte *)image.getBasePtr(0, yy);
+					outside = row[x] == transparentColor;
+				}
+				sum += kernel[k + 2] * (outside ? center : tmp[yy * width + x]);
+			}
+			dstRow[x] = (sum + 32) >> 6;
+		}
+	}
+}
+
+bool isPaperScanImage(const Common::Path &path) {
+	Common::String pathString = path.toString('/');
+	pathString.toLowercase();
+
+	size_t fileNamePos = pathString.findLastOf('/');
+	Common::String fileName = (fileNamePos == Common::String::npos) ?
+		pathString : pathString.substr(fileNamePos + 1);
+
+	if (!fileName.hasSuffix(".bmp"))
+		return false;
+
+	if (fileName.contains("mask") || fileName.contains("msk") || fileName.contains("mk"))
+		return false;
+
+	if (pathString.contains("inface/dossiers/") && fileName.hasPrefix("filec"))
+		return true;
+
+	if (!pathString.contains("/search_s/"))
+		return false;
+
+	return fileName.contains("nwsc") ||
+	       fileName.contains("newsc") ||
+	       fileName.contains("magc") ||
+	       fileName.contains("formcu") ||
+	       fileName.contains("ltrcu") ||
+	       fileName.contains("liccu") ||
+	       fileName.contains("billcu") ||
+	       fileName.contains("bilcu");
+}
+
+// Readability enhancement for document close-ups (newspapers, magazines,
+// letters): stretch the luminance histogram so it spans the full range
+// (clipping 1% of pixels at each end) and apply a gentle unsharp mask.
+//
+// The paper in these scans is dithered with alternately tinted entries
+// that read as colored speckle once the contrast is raised, so the
+// chroma is smoothed over a small neighborhood and every pixel is mapped
+// to the palette entry nearest to (new luminance, smoothed chroma),
+// which collapses the dither into a uniform paper tint.
+//
+// The palette itself is never modified, and pixels are only remapped to
+// palette indices the image already uses: the palette installed when the
+// image is drawn is not necessarily this image's own (scene images share
+// index conventions), so an index this image never referenced may show
+// up as an arbitrary color on screen even though it looks correct in the
+// image's own palette.
+bool enhancePaperScanImage(Graphics::Surface *image, const byte *palette, byte transparentColor) {
+	if (image->format.bytesPerPixel != 1)
+		return false;
+
+	const int width = image->w;
+	const int height = image->h;
+
+	byte luma[Graphics::PALETTE_COUNT], chromaB[Graphics::PALETTE_COUNT], chromaR[Graphics::PALETTE_COUNT];
+	for (uint32 color = 0; color < Graphics::PALETTE_COUNT; color++) {
+		int r = palette[3 * color + 0];
+		int g = palette[3 * color + 1];
+		int b = palette[3 * color + 2];
+		luma[color] = (77 * r + 150 * g + 29 * b) >> 8;
+		chromaB[color] = (byte)(128 + (-43 * r - 85 * g + 128 * b) / 256);
+		chromaR[color] = (byte)(128 + (128 * r - 107 * g - 21 * b) / 256);
+	}
+
+	uint32 histogram[Graphics::PALETTE_COUNT] = {};
+	bool used[Graphics::PALETTE_COUNT] = {};
+	uint32 opaquePixelCount = 0;
+	uint32 histogramPixelCount = 0;
+
+	for (int y = 0; y < height; y++) {
+		const byte *row = (const byte *)image->getBasePtr(0, y);
+		for (int x = 0; x < width; x++) {
+			byte color = row[x];
+			if (color == transparentColor) {
+				histogram[kPaperScanTransparentBackgroundLuma]++;
+				histogramPixelCount++;
+				continue;
+			}
+
+			histogram[luma[color]]++;
+			used[color] = true;
+			opaquePixelCount++;
+			histogramPixelCount++;
+		}
+	}
+
+	if (opaquePixelCount < (uint32)(width * height / 30))
+		return false;
+
+	Common::Array<byte> allowedColors;
+	allowedColors.reserve(Graphics::PALETTE_COUNT);
+	for (uint32 color = 0; color < Graphics::PALETTE_COUNT; color++) {
+		if (used[color] && color != transparentColor)
+			allowedColors.push_back(color);
+	}
+	if (allowedColors.empty())
+		return false;
+
+	uint32 cutoff = histogramPixelCount / 100;
+	uint32 accumulated = 0;
+	int lowCut = 0;
+	while (lowCut < 255 && accumulated + histogram[lowCut] <= cutoff)
+		accumulated += histogram[lowCut++];
+
+	accumulated = 0;
+	int highCut = 255;
+	while (highCut > lowCut && accumulated + histogram[highCut] <= cutoff)
+		accumulated += histogram[highCut--];
+
+	if (highCut <= lowCut) {
+		lowCut = 0;
+		highCut = 255;
+	}
+
+	byte stretched[Graphics::PALETTE_COUNT];
+	for (int level = 0; level < Graphics::PALETTE_COUNT; level++)
+		stretched[level] = (byte)CLIP((level - lowCut) * 255 / (highCut - lowCut), 0, 255);
+
+	const uint32 planeSize = width * height;
+	Common::Array<byte> yPlane(planeSize), yBlur(planeSize);
+	Common::Array<byte> cbPlane(planeSize), crPlane(planeSize);
+	Common::Array<byte> scratch(planeSize);
+	const uint32 cacheSize = Graphics::PALETTE_COUNT * 64 * 64;
+	Common::Array<int16> nearestCache(cacheSize, -1);
+
+	for (int y = 0; y < height; y++) {
+		const byte *row = (const byte *)image->getBasePtr(0, y);
+		byte *yRow = yPlane.data() + y * width;
+		byte *cbRow = cbPlane.data() + y * width;
+		byte *crRow = crPlane.data() + y * width;
+		for (int x = 0; x < width; x++) {
+			byte color = row[x];
+			yRow[x] = stretched[luma[color]];
+			cbRow[x] = chromaB[color];
+			crRow[x] = chromaR[color];
+		}
+	}
+
+	blurPlane(*image, transparentColor, yPlane.data(), yBlur.data(), scratch.data());
+	blurPlane(*image, transparentColor, cbPlane.data(), cbPlane.data(), scratch.data());
+	blurPlane(*image, transparentColor, cbPlane.data(), cbPlane.data(), scratch.data());
+	blurPlane(*image, transparentColor, crPlane.data(), crPlane.data(), scratch.data());
+	blurPlane(*image, transparentColor, crPlane.data(), crPlane.data(), scratch.data());
+
+	for (int y = 0; y < height; y++) {
+		byte *row = (byte *)image->getBasePtr(0, y);
+		const byte *yRow = yPlane.data() + y * width;
+		const byte *blurRow = yBlur.data() + y * width;
+		const byte *cbRow = cbPlane.data() + y * width;
+		const byte *crRow = crPlane.data() + y * width;
+		for (int x = 0; x < width; x++) {
+			byte color = row[x];
+			if (color == transparentColor)
+				continue;
+
+			int value = yRow[x];
+			int diff = value - blurRow[x];
+			if (diff > 2 || diff < -2)
+				value += diff * 120 / 100;
+			value = CLIP(value, 0, 255);
+
+			int targetCb = (cbRow[x] & ~3) + 2;
+			int targetCr = (crRow[x] & ~3) + 2;
+
+			uint32 cacheKey = (value << 12) | ((cbRow[x] >> 2) << 6) | (crRow[x] >> 2);
+			if (nearestCache[cacheKey] < 0)
+				nearestCache[cacheKey] = findNearestUsedColor(luma, chromaB, chromaR,
+					allowedColors, value, targetCb, targetCr);
+			row[x] = (byte)nearestCache[cacheKey];
+		}
+	}
+
+	return true;
+}
+
+} // End of namespace Private
diff --git a/engines/private/paper.h b/engines/private/paper.h
new file mode 100644
index 00000000000..c7b3d07bc03
--- /dev/null
+++ b/engines/private/paper.h
@@ -0,0 +1,42 @@
+/* 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/>.
+ *
+ */
+
+#ifndef PRIVATE_PAPER_H
+#define PRIVATE_PAPER_H
+
+#include "common/scummsys.h"
+
+namespace Common {
+class Path;
+}
+
+namespace Graphics {
+struct Surface;
+}
+
+namespace Private {
+
+bool isPaperScanImage(const Common::Path &path);
+bool enhancePaperScanImage(Graphics::Surface *image, const byte *palette, byte transparentColor);
+
+} // End of namespace Private
+
+#endif // PRIVATE_PAPER_H
diff --git a/engines/private/private.cpp b/engines/private/private.cpp
index 45c82bcee1c..f833cd17e73 100644
--- a/engines/private/private.cpp
+++ b/engines/private/private.cpp
@@ -42,6 +42,7 @@
 
 #include "private/decompiler.h"
 #include "private/grammar.h"
+#include "private/paper.h"
 #include "private/private.h"
 #include "private/savegame.h"
 #include "private/tokens.h"
@@ -59,6 +60,9 @@ PrivateEngine::PrivateEngine(OSystem *syst, const ADGameDescription *gd)
 	  _defaultCursor(nullptr),
 	  _screenW(640), _screenH(480) {
 	_highlightMasks = false;
+	_readingMaterialContrast = false;
+	_paperScanFilteringActive = false;
+	_paperScanPreviousFiltering = false;
 	_rnd = new Common::RandomSource("private");
 
 	// Global object for external reference
@@ -130,6 +134,7 @@ PrivateEngine::PrivateEngine(OSystem *syst, const ADGameDescription *gd)
 }
 
 PrivateEngine::~PrivateEngine() {
+	setPaperScanFiltering(false);
 	destroyVideo();
 	destroySubtitles();
 
@@ -273,6 +278,9 @@ Common::Error PrivateEngine::run() {
 	if (!Common::parseBool(ConfMan.get("highlightMasks"), _shouldHighlightMasks))
 		warning("Failed to parse bool from highlightMasks options");
 
+	if (!Common::parseBool(ConfMan.get("readingMaterialContrast"), _readingMaterialContrast))
+		warning("Failed to parse bool from readingMaterialContrast options");
+
 	if (!_useSubtitles && _sfxSubtitles) {
 		warning("SFX subtitles are enabled, but no subtitles will be shown");
 	}
@@ -515,6 +523,8 @@ void PrivateEngine::initFuncs() {
 }
 
 void PrivateEngine::clearAreas() {
+	setPaperScanFiltering(false);
+
 	for (MaskList::const_iterator it = _masks.begin(); it != _masks.end(); ++it) {
 		const MaskInfo &m = *it;
 		if (m.surf != nullptr) {
@@ -2766,6 +2776,11 @@ Graphics::Surface *PrivateEngine::decodeImage(const Common::String &name, byte *
 		swapImageColors(newImage, currentPalette, maskTransparentColor, _transparentColor);
 	}
 
+	if (_readingMaterialContrast && isPaperScanImage(path)) {
+		if (enhancePaperScanImage(newImage, currentPalette, _transparentColor))
+			setPaperScanFiltering(true);
+	}
+
 	return newImage;
 }
 
@@ -2854,6 +2869,31 @@ void PrivateEngine::swapImageColors(Graphics::Surface *image, byte *palette, uin
 	}
 }
 
+void PrivateEngine::setPaperScanFiltering(bool enabled) {
+	if (!_system->hasFeature(OSystem::kFeatureFilteringMode))
+		return;
+
+	if (enabled) {
+		if (_paperScanFilteringActive)
+			return;
+
+		_paperScanPreviousFiltering = _system->getFeatureState(OSystem::kFeatureFilteringMode);
+		if (!_paperScanPreviousFiltering) {
+			_system->beginGFXTransaction();
+			_system->setFeatureState(OSystem::kFeatureFilteringMode, true);
+			_system->endGFXTransaction();
+		}
+		_paperScanFilteringActive = true;
+	} else if (_paperScanFilteringActive) {
+		if (_system->getFeatureState(OSystem::kFeatureFilteringMode) != _paperScanPreviousFiltering) {
+			_system->beginGFXTransaction();
+			_system->setFeatureState(OSystem::kFeatureFilteringMode, _paperScanPreviousFiltering);
+			_system->endGFXTransaction();
+		}
+		_paperScanFilteringActive = false;
+	}
+}
+
 void PrivateEngine::loadImage(const Common::String &name, int x, int y) {
 	debugC(1, kPrivateDebugFunction, "%s(%s,%d,%d)", __FUNCTION__, name.c_str(), x, y);
 	byte *palette;
diff --git a/engines/private/private.h b/engines/private/private.h
index d9f25a3c236..ee13e68f453 100644
--- a/engines/private/private.h
+++ b/engines/private/private.h
@@ -257,6 +257,7 @@ private:
 public:
 	bool _shouldHighlightMasks;
 	bool _highlightMasks;
+	bool _readingMaterialContrast;
 	PrivateEngine(OSystem *syst, const ADGameDescription *gd);
 	~PrivateEngine();
 
@@ -335,6 +336,7 @@ public:
 	void remapImage(uint16 ncolors, const Graphics::Surface *oldImage, const byte *oldPalette, Graphics::Surface *newImage, const byte *currentPalette);
 	static uint32 findMaskTransparentColor(const byte *palette, uint32 defaultColor);
 	static void swapImageColors(Graphics::Surface *image, byte *palette, uint32 a, uint32 b);
+	void setPaperScanFiltering(bool enabled);
 	void loadImage(const Common::String &file, int x, int y);
 	void drawScreenFrame(const byte *videoPalette);
 
@@ -365,6 +367,8 @@ public:
 	Common::Point _origin;
 	void drawScreen();
 	bool _needToDrawScreenFrame;
+	bool _paperScanFilteringActive;
+	bool _paperScanPreviousFiltering;
 
 	// settings
 	Common::String _nextSetting;




More information about the Scummvm-git-logs mailing list