1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "graphics/thumbnail.h"
24 #include "graphics/scaler.h"
25 #include "graphics/colormasks.h"
26 #include "common/endian.h"
27 #include "common/algorithm.h"
28 #include "common/system.h"
29 #include "common/stream.h"
30 #include "common/textconsole.h"
31 
32 namespace Graphics {
33 
34 namespace {
35 #define THMB_VERSION 2
36 
37 struct ThumbnailHeader {
38 	uint32 type;
39 	uint32 size;
40 	byte version;
41 	uint16 width, height;
42 	PixelFormat format;
43 };
44 
45 #define ThumbnailHeaderSize (4+4+1+2+2+(1+4+4))
46 
47 enum HeaderState {
48 	/// There is no header present
49 	kHeaderNone,
50 	/// The header present only has reliable values for version and size
51 	kHeaderUnsupported,
52 	/// The header is present and the version is supported
53 	kHeaderPresent
54 };
55 
loadHeader(Common::SeekableReadStream & in,ThumbnailHeader & header,bool outputWarnings)56 HeaderState loadHeader(Common::SeekableReadStream &in, ThumbnailHeader &header, bool outputWarnings) {
57 	header.type = in.readUint32BE();
58 	// We also accept the bad 'BMHT' header here, for the sake of compatibility
59 	// with some older savegames which were written incorrectly due to a bug in
60 	// ScummVM which wrote the thumb header type incorrectly on LE systems.
61 	if (header.type != MKTAG('T','H','M','B') && header.type != MKTAG('B','M','H','T')) {
62 		if (outputWarnings)
63 			warning("couldn't find thumbnail header type");
64 		return kHeaderNone;
65 	}
66 
67 	header.size = in.readUint32BE();
68 	header.version = in.readByte();
69 
70 	// Do a check whether any read errors had occurred. If so we cannot use the
71 	// values obtained for size and version because they might be bad.
72 	if (in.err() || in.eos()) {
73 		// TODO: We fake that there is no header. This is actually not quite
74 		// correct since we found the start of the header and then things
75 		// started to break. Right no we leave detection of this to the client.
76 		// Since this case is caused by broken files, the client code should
77 		// catch it anyway... If there is a nicer solution here, we should
78 		// implement it.
79 		return kHeaderNone;
80 	}
81 
82 	if (header.version > THMB_VERSION) {
83 		if (outputWarnings)
84 			warning("trying to load a newer thumbnail version: %d instead of %d", header.version, THMB_VERSION);
85 		return kHeaderUnsupported;
86 	}
87 
88 	header.width = in.readUint16BE();
89 	header.height = in.readUint16BE();
90 	header.format.bytesPerPixel = in.readByte();
91 	// Starting from version 2 on we serialize the whole PixelFormat.
92 	if (header.version >= 2) {
93 		header.format.rLoss = in.readByte();
94 		header.format.gLoss = in.readByte();
95 		header.format.bLoss = in.readByte();
96 		header.format.aLoss = in.readByte();
97 
98 		header.format.rShift = in.readByte();
99 		header.format.gShift = in.readByte();
100 		header.format.bShift = in.readByte();
101 		header.format.aShift = in.readByte();
102 	} else {
103 		// Version 1 used a hardcoded RGB565.
104 		header.format = createPixelFormat<565>();
105 	}
106 
107 	if (in.err() || in.eos()) {
108 		// When we reached this point we know that at least the size and
109 		// version field was loaded successfully, thus we tell this header
110 		// is not supported and silently hope that the client code is
111 		// prepared to handle read errors.
112 		return kHeaderUnsupported;
113 	} else {
114 		return kHeaderPresent;
115 	}
116 }
117 } // end of anonymous namespace
118 
checkThumbnailHeader(Common::SeekableReadStream & in)119 bool checkThumbnailHeader(Common::SeekableReadStream &in) {
120 	uint32 position = in.pos();
121 	ThumbnailHeader header;
122 
123 	// TODO: It is not clear whether this is the best semantics. Now
124 	// checkThumbnailHeader will return true even when the thumbnail header
125 	// found is actually not usable. However, most engines seem to use this
126 	// to detect the presence of any header and if there is none it wont even
127 	// try to skip it. Thus, this looks like the best solution for now...
128 	bool hasHeader = (loadHeader(in, header, false) != kHeaderNone);
129 
130 	in.seek(position, SEEK_SET);
131 
132 	return hasHeader;
133 }
134 
skipThumbnail(Common::SeekableReadStream & in)135 bool skipThumbnail(Common::SeekableReadStream &in) {
136 	uint32 position = in.pos();
137 	ThumbnailHeader header;
138 
139 	// We can skip unsupported and supported headers. So we only seek back
140 	// to the old position in case there is no header at all.
141 	if (loadHeader(in, header, false) == kHeaderNone) {
142 		in.seek(position, SEEK_SET);
143 		return false;
144 	}
145 
146 	in.seek(header.size - (in.pos() - position), SEEK_CUR);
147 	return true;
148 }
149 
loadThumbnail(Common::SeekableReadStream & in)150 Graphics::Surface *loadThumbnail(Common::SeekableReadStream &in) {
151 	const uint32 position = in.pos();
152 	ThumbnailHeader header;
153 	HeaderState headerState = loadHeader(in, header, true);
154 
155 	// Try to handle unsupported/broken headers gracefully. If there is no
156 	// header at all, we seek back and return at this point. If there is an
157 	// unsupported/broken header, we skip the actual data and return. The
158 	// downside is that we might reset the end of stream flag with this and
159 	// the client code would not be able to notice a read past the end of the
160 	// stream at this point then.
161 	if (headerState == kHeaderNone) {
162 		in.seek(position, SEEK_SET);
163 		return 0;
164 	} else if (headerState == kHeaderUnsupported) {
165 		in.seek(header.size - (in.pos() - position), SEEK_CUR);
166 		return 0;
167 	}
168 
169 	if (header.format.bytesPerPixel != 2 && header.format.bytesPerPixel != 4) {
170 		warning("trying to load thumbnail with unsupported bit depth %d", header.format.bytesPerPixel);
171 		return 0;
172 	}
173 
174 	Graphics::Surface *const to = new Graphics::Surface();
175 	to->create(header.width, header.height, header.format);
176 
177 	for (int y = 0; y < to->h; ++y) {
178 		switch (header.format.bytesPerPixel) {
179 		case 2: {
180 			uint16 *pixels = (uint16 *)to->getBasePtr(0, y);
181 			for (uint x = 0; x < to->w; ++x) {
182 				*pixels++ = in.readUint16BE();
183 			}
184 			} break;
185 
186 		case 4: {
187 			uint32 *pixels = (uint32 *)to->getBasePtr(0, y);
188 			for (uint x = 0; x < to->w; ++x) {
189 				*pixels++ = in.readUint32BE();
190 			}
191 			} break;
192 
193 		default:
194 			assert(0);
195 		}
196 	}
197 	return to;
198 }
199 
saveThumbnail(Common::WriteStream & out)200 bool saveThumbnail(Common::WriteStream &out) {
201 	Graphics::Surface thumb;
202 
203 	if (!createThumbnailFromScreen(&thumb)) {
204 		warning("Couldn't create thumbnail from screen, aborting thumbnail save");
205 		return false;
206 	}
207 
208 	bool success = saveThumbnail(out, thumb);
209 	thumb.free();
210 
211 	return success;
212 }
213 
saveThumbnail(Common::WriteStream & out,const Graphics::Surface & thumb)214 bool saveThumbnail(Common::WriteStream &out, const Graphics::Surface &thumb) {
215 	if (thumb.format.bytesPerPixel != 2 && thumb.format.bytesPerPixel != 4) {
216 		warning("trying to save thumbnail with bpp %u", thumb.format.bytesPerPixel);
217 		return false;
218 	}
219 
220 	ThumbnailHeader header;
221 	header.type = MKTAG('T','H','M','B');
222 	header.size = ThumbnailHeaderSize + thumb.w*thumb.h*thumb.format.bytesPerPixel;
223 	header.version = THMB_VERSION;
224 	header.width = thumb.w;
225 	header.height = thumb.h;
226 
227 	out.writeUint32BE(header.type);
228 	out.writeUint32BE(header.size);
229 	out.writeByte(header.version);
230 	out.writeUint16BE(header.width);
231 	out.writeUint16BE(header.height);
232 
233 	// Serialize the PixelFormat
234 	out.writeByte(thumb.format.bytesPerPixel);
235 	out.writeByte(thumb.format.rLoss);
236 	out.writeByte(thumb.format.gLoss);
237 	out.writeByte(thumb.format.bLoss);
238 	out.writeByte(thumb.format.aLoss);
239 	out.writeByte(thumb.format.rShift);
240 	out.writeByte(thumb.format.gShift);
241 	out.writeByte(thumb.format.bShift);
242 	out.writeByte(thumb.format.aShift);
243 
244 	// Serialize the pixel data
245 	for (uint y = 0; y < thumb.h; ++y) {
246 		switch (thumb.format.bytesPerPixel) {
247 		case 2: {
248 			const uint16 *pixels = (const uint16 *)thumb.getBasePtr(0, y);
249 			for (uint x = 0; x < thumb.w; ++x) {
250 				out.writeUint16BE(*pixels++);
251 			}
252 			} break;
253 
254 		case 4: {
255 			const uint32 *pixels = (const uint32 *)thumb.getBasePtr(0, y);
256 			for (uint x = 0; x < thumb.w; ++x) {
257 				out.writeUint32BE(*pixels++);
258 			}
259 			} break;
260 
261 		default:
262 			assert(0);
263 		}
264 	}
265 
266 	return true;
267 }
268 
269 
270 /**
271  * Returns an array indicating which pixels of a source image horizontally or vertically get
272  * included in a scaled image
273  */
scaleLine(int size,int srcSize)274 int *scaleLine(int size, int srcSize) {
275 	int scale = 100 * size / srcSize;
276 	assert(scale > 0);
277 	int *v = new int[size];
278 	Common::fill(v, &v[size], 0);
279 
280 	int distCtr = 0;
281 	int *destP = v;
282 	for (int distIndex = 0; distIndex < srcSize; ++distIndex) {
283 		distCtr += scale;
284 		while (distCtr >= 100) {
285 			assert(destP < &v[size]);
286 			*destP++ = distIndex;
287 			distCtr -= 100;
288 		}
289 	}
290 
291 	return v;
292 }
293 
scale(const Graphics::Surface & srcImage,int xSize,int ySize)294 Graphics::Surface *scale(const Graphics::Surface &srcImage, int xSize, int ySize) {
295 	Graphics::Surface *s = new Graphics::Surface();
296 	s->create(xSize, ySize, srcImage.format);
297 
298 	int *horizUsage = scaleLine(xSize, srcImage.w);
299 	int *vertUsage = scaleLine(ySize, srcImage.h);
300 
301 	// Loop to create scaled version
302 	for (int yp = 0; yp < ySize; ++yp) {
303 		const byte *srcP = (const byte *)srcImage.getBasePtr(0, vertUsage[yp]);
304 		byte *destP = (byte *)s->getBasePtr(0, yp);
305 
306 		for (int xp = 0; xp < xSize; ++xp) {
307 			const byte *tempSrcP = srcP + (horizUsage[xp] * srcImage.format.bytesPerPixel);
308 			for (int byteCtr = 0; byteCtr < srcImage.format.bytesPerPixel; ++byteCtr) {
309 				*destP++ = *tempSrcP++;
310 			}
311 		}
312 	}
313 
314 	// Delete arrays and return surface
315 	delete[] horizUsage;
316 	delete[] vertUsage;
317 	return s;
318 }
319 
320 } // End of namespace Graphics
321