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 "common/scummsys.h"
24 #include "common/file.h"
25 #include "common/tokenizer.h"
26 #include "common/debug.h"
27 #include "common/rect.h"
28 #include "graphics/fontman.h"
29 #include "graphics/colormasks.h"
30 #include "graphics/surface.h"
31 #include "graphics/font.h"
32 #include "graphics/fonts/ttf.h"
33 
34 #include "zvision/text/text.h"
35 #include "zvision/graphics/render_manager.h"
36 #include "zvision/text/truetype_font.h"
37 #include "zvision/scripting/script_manager.h"
38 
39 namespace ZVision {
40 
TextStyleState()41 TextStyleState::TextStyleState() {
42 	_fontname = "Arial";
43 	_blue = 255;
44 	_green = 255;
45 	_red = 255;
46 	_bold = false;
47 #if 0
48 	_newline = false;
49 	_escapement = 0;
50 #endif
51 	_italic = false;
52 	_justification = TEXT_JUSTIFY_LEFT;
53 	_size = 12;
54 #if 0
55 	_skipcolor = false;
56 #endif
57 	_strikeout = false;
58 	_underline = false;
59 	_statebox = 0;
60 	_sharp = false;
61 }
62 
parseStyle(const Common::String & str,int16 len)63 TextChange TextStyleState::parseStyle(const Common::String &str, int16 len) {
64 	Common::String buf = Common::String(str.c_str(), len);
65 
66 	uint retval = TEXT_CHANGE_NONE;
67 
68 	Common::StringTokenizer tokenizer(buf, " ");
69 	Common::String token;
70 
71 	while (!tokenizer.empty()) {
72 		token = tokenizer.nextToken();
73 
74 		if (token.matchString("font", true)) {
75 			token = tokenizer.nextToken();
76 			if (token[0] == '"') {
77 				Common::String _tmp = Common::String(token.c_str() + 1);
78 
79 				while (token.lastChar() != '"' && !tokenizer.empty()) {
80 					token = tokenizer.nextToken();
81 					_tmp += " " + token;
82 				}
83 
84 				if (_tmp.lastChar() == '"')
85 					_tmp.deleteLastChar();
86 
87 				_fontname = _tmp;
88 			} else {
89 				if (!tokenizer.empty())
90 					_fontname = token;
91 			}
92 			retval |= TEXT_CHANGE_FONT_TYPE;
93 
94 		} else if (token.matchString("blue", true)) {
95 			if (!tokenizer.empty()) {
96 				token = tokenizer.nextToken();
97 				int32 tmp = atoi(token.c_str());
98 				if (_blue != tmp) {
99 					_blue = tmp;
100 					retval |= TEXT_CHANGE_FONT_STYLE;
101 				}
102 			}
103 		} else if (token.matchString("red", true)) {
104 			if (!tokenizer.empty()) {
105 				token = tokenizer.nextToken();
106 				int32 tmp = atoi(token.c_str());
107 				if (_red != tmp) {
108 					_red = tmp;
109 					retval |= TEXT_CHANGE_FONT_STYLE;
110 				}
111 			}
112 		} else if (token.matchString("green", true)) {
113 			if (!tokenizer.empty()) {
114 				token = tokenizer.nextToken();
115 				int32 tmp = atoi(token.c_str());
116 				if (_green != tmp) {
117 					_green = tmp;
118 					retval |= TEXT_CHANGE_FONT_STYLE;
119 				}
120 			}
121 		} else if (token.matchString("newline", true)) {
122 #if 0
123 			if ((retval & TXT_RET_NEWLN) == 0)
124 				_newline = 0;
125 
126 			_newline++;
127 #endif
128 			retval |= TEXT_CHANGE_NEWLINE;
129 		} else if (token.matchString("point", true)) {
130 			if (!tokenizer.empty()) {
131 				token = tokenizer.nextToken();
132 				int32 tmp = atoi(token.c_str());
133 				if (_size != tmp) {
134 					_size = tmp;
135 					retval |= TEXT_CHANGE_FONT_TYPE;
136 				}
137 			}
138 		} else if (token.matchString("escapement", true)) {
139 			if (!tokenizer.empty()) {
140 				token = tokenizer.nextToken();
141 #if 0
142 				int32 tmp = atoi(token.c_str());
143 				_escapement = tmp;
144 #endif
145 			}
146 		} else if (token.matchString("italic", true)) {
147 			if (!tokenizer.empty()) {
148 				token = tokenizer.nextToken();
149 				if (token.matchString("on", true)) {
150 					if (_italic != true) {
151 						_italic = true;
152 						retval |= TEXT_CHANGE_FONT_STYLE;
153 					}
154 				} else if (token.matchString("off", true)) {
155 					if (_italic != false) {
156 						_italic = false;
157 						retval |= TEXT_CHANGE_FONT_STYLE;
158 					}
159 				}
160 			}
161 		} else if (token.matchString("underline", true)) {
162 			if (!tokenizer.empty()) {
163 				token = tokenizer.nextToken();
164 				if (token.matchString("on", true)) {
165 					if (_underline != true) {
166 						_underline = true;
167 						retval |= TEXT_CHANGE_FONT_STYLE;
168 					}
169 				} else if (token.matchString("off", true)) {
170 					if (_underline != false) {
171 						_underline = false;
172 						retval |= TEXT_CHANGE_FONT_STYLE;
173 					}
174 				}
175 			}
176 		} else if (token.matchString("strikeout", true)) {
177 			if (!tokenizer.empty()) {
178 				token = tokenizer.nextToken();
179 				if (token.matchString("on", true)) {
180 					if (_strikeout != true) {
181 						_strikeout = true;
182 						retval |= TEXT_CHANGE_FONT_STYLE;
183 					}
184 				} else if (token.matchString("off", true)) {
185 					if (_strikeout != false) {
186 						_strikeout = false;
187 						retval |= TEXT_CHANGE_FONT_STYLE;
188 					}
189 				}
190 			}
191 		} else if (token.matchString("bold", true)) {
192 			if (!tokenizer.empty()) {
193 				token = tokenizer.nextToken();
194 				if (token.matchString("on", true)) {
195 					if (_bold != true) {
196 						_bold = true;
197 						retval |= TEXT_CHANGE_FONT_STYLE;
198 					}
199 				} else if (token.matchString("off", true)) {
200 					if (_bold != false) {
201 						_bold = false;
202 						retval |= TEXT_CHANGE_FONT_STYLE;
203 					}
204 				}
205 			}
206 		} else if (token.matchString("skipcolor", true)) {
207 			if (!tokenizer.empty()) {
208 				token = tokenizer.nextToken();
209 #if 0
210 				if (token.matchString("on", true)) {
211 					_skipcolor = true;
212 				} else if (token.matchString("off", true)) {
213 					_skipcolor = false;
214 				}
215 #endif
216 			}
217 		} else if (token.matchString("image", true)) {
218 			// Not used
219 		} else if (token.matchString("statebox", true)) {
220 			if (!tokenizer.empty()) {
221 				token = tokenizer.nextToken();
222 				_statebox = atoi(token.c_str());
223 				retval |= TEXT_CHANGE_HAS_STATE_BOX;
224 			}
225 		} else if (token.matchString("justify", true)) {
226 			if (!tokenizer.empty()) {
227 				token = tokenizer.nextToken();
228 				if (token.matchString("center", true))
229 					_justification = TEXT_JUSTIFY_CENTER;
230 				else if (token.matchString("left", true))
231 					_justification = TEXT_JUSTIFY_LEFT;
232 				else if (token.matchString("right", true))
233 					_justification = TEXT_JUSTIFY_RIGHT;
234 			}
235 		}
236 	}
237 	return (TextChange)retval;
238 }
239 
readAllStyles(const Common::String & txt)240 void TextStyleState::readAllStyles(const Common::String &txt) {
241 	int16 startTextPosition = -1;
242 	int16 endTextPosition = -1;
243 
244 	for (uint16 i = 0; i < txt.size(); i++) {
245 		if (txt[i] == '<')
246 			startTextPosition = i;
247 		else if (txt[i] == '>') {
248 			endTextPosition = i;
249 			if (startTextPosition != -1) {
250 				if ((endTextPosition - startTextPosition - 1) > 0) {
251 					parseStyle(Common::String(txt.c_str() + startTextPosition + 1), endTextPosition - startTextPosition - 1);
252 				}
253 			}
254 		}
255 
256 	}
257 }
258 
updateFontWithTextState(StyledTTFont & font)259 void TextStyleState::updateFontWithTextState(StyledTTFont &font) {
260 	uint tempStyle = 0;
261 
262 	if (_bold) {
263 		tempStyle |= StyledTTFont::TTF_STYLE_BOLD;
264 	}
265 	if (_italic) {
266 		tempStyle |= StyledTTFont::TTF_STYLE_ITALIC;
267 	}
268 	if (_underline) {
269 		tempStyle |= StyledTTFont::TTF_STYLE_UNDERLINE;
270 	}
271 	if (_strikeout) {
272 		tempStyle |= StyledTTFont::TTF_STYLE_STRIKETHROUGH;
273 	}
274 	if (_sharp) {
275 		tempStyle |= StyledTTFont::TTF_STYLE_SHARP;
276 	}
277 
278 	font.loadFont(_fontname, _size, tempStyle);
279 }
280 
drawTextWithJustification(const Common::String & text,StyledTTFont & font,uint32 color,Graphics::Surface & dest,int lineY,TextJustification justify)281 void TextRenderer::drawTextWithJustification(const Common::String &text, StyledTTFont &font, uint32 color, Graphics::Surface &dest, int lineY, TextJustification justify) {
282 	if (justify == TEXT_JUSTIFY_LEFT)
283 		font.drawString(&dest, text, 0, lineY, dest.w, color, Graphics::kTextAlignLeft);
284 	else if (justify == TEXT_JUSTIFY_CENTER)
285 		font.drawString(&dest, text, 0, lineY, dest.w, color, Graphics::kTextAlignCenter);
286 	else if (justify == TEXT_JUSTIFY_RIGHT)
287 		font.drawString(&dest, text, 0, lineY, dest.w, color, Graphics::kTextAlignRight);
288 }
289 
drawText(const Common::String & text,TextStyleState & state,Graphics::Surface & dest)290 int32 TextRenderer::drawText(const Common::String &text, TextStyleState &state, Graphics::Surface &dest) {
291 	StyledTTFont font(_engine);
292 	state.updateFontWithTextState(font);
293 
294 	uint32 color = _engine->_resourcePixelFormat.RGBToColor(state._red, state._green, state._blue);
295 	drawTextWithJustification(text, font, color, dest, 0, state._justification);
296 
297 	return font.getStringWidth(text);
298 }
299 
300 struct TextSurface {
TextSurfaceZVision::TextSurface301 	TextSurface(Graphics::Surface *surface, Common::Point surfaceOffset, uint lineNumber)
302 		: _surface(surface),
303 		  _surfaceOffset(surfaceOffset),
304 		  _lineNumber(lineNumber) {
305 	}
306 
307 	Graphics::Surface *_surface;
308 	Common::Point _surfaceOffset;
309 	uint _lineNumber;
310 };
311 
drawTextWithWordWrapping(const Common::String & text,Graphics::Surface & dest)312 void TextRenderer::drawTextWithWordWrapping(const Common::String &text, Graphics::Surface &dest) {
313 	Common::Array<TextSurface> textSurfaces;
314 	Common::Array<uint> lineWidths;
315 	Common::Array<TextJustification> lineJustifications;
316 
317 	// Create the initial text state
318 	TextStyleState currentState;
319 
320 	// Create an empty font and bind it to the state
321 	StyledTTFont font(_engine);
322 	currentState.updateFontWithTextState(font);
323 
324 	Common::String currentSentence; // Not a true 'grammatical' sentence. Rather, it's just a collection of words
325 	Common::String currentWord;
326 	int sentenceWidth = 0;
327 	int wordWidth = 0;
328 	int lineWidth = 0;
329 	int lineHeight = font.getFontHeight();
330 
331 	uint currentLineNumber = 0u;
332 
333 	uint numSpaces = 0u;
334 	int spaceWidth = 0;
335 
336 	// The pixel offset to the currentSentence
337 	Common::Point sentencePixelOffset;
338 
339 	uint i = 0u;
340 	uint stringlen = text.size();
341 
342 	while (i < stringlen) {
343 		if (text[i] == '<') {
344 			// Flush the currentWord to the currentSentence
345 			currentSentence += currentWord;
346 			sentenceWidth += wordWidth;
347 
348 			// Reset the word variables
349 			currentWord.clear();
350 			wordWidth = 0;
351 
352 			// Parse the style tag
353 			uint startTextPosition = i;
354 			while (i < stringlen && text[i] != '>') {
355 				++i;
356 			}
357 			uint endTextPosition = i;
358 
359 			uint32 textColor = currentState.getTextColor(_engine);
360 
361 			uint stateChanges = 0u;
362 			if ((endTextPosition - startTextPosition - 1) > 0) {
363 				stateChanges = currentState.parseStyle(Common::String(text.c_str() + startTextPosition + 1), endTextPosition - startTextPosition - 1);
364 			}
365 
366 			if (stateChanges & (TEXT_CHANGE_FONT_TYPE | TEXT_CHANGE_FONT_STYLE)) {
367 				// Use the last state to render out the current sentence
368 				// Styles apply to the text 'after' them
369 				if (!currentSentence.empty()) {
370 					textSurfaces.push_back(TextSurface(font.renderSolidText(currentSentence, textColor), sentencePixelOffset, currentLineNumber));
371 
372 					lineWidth += sentenceWidth;
373 					sentencePixelOffset.x += sentenceWidth;
374 
375 					// Reset the sentence variables
376 					currentSentence.clear();
377 					sentenceWidth = 0;
378 				}
379 
380 				// Update the current state with the style information
381 				currentState.updateFontWithTextState(font);
382 
383 				lineHeight = MAX(lineHeight, font.getFontHeight());
384 				spaceWidth = font.getCharWidth(' ');
385 			}
386 			if (stateChanges & TEXT_CHANGE_NEWLINE) {
387 				// If the current sentence has content, render it out
388 				if (!currentSentence.empty()) {
389 					textSurfaces.push_back(TextSurface(font.renderSolidText(currentSentence, textColor), sentencePixelOffset, currentLineNumber));
390 				}
391 
392 				// Set line width
393 				lineWidths.push_back(lineWidth + sentenceWidth - (numSpaces * spaceWidth));
394 
395 				currentSentence.clear();
396 				sentenceWidth = 0;
397 
398 				// Update the offsets
399 				sentencePixelOffset.x = 0u;
400 				sentencePixelOffset.y += lineHeight;
401 
402 				// Reset the line variables
403 				lineHeight = font.getFontHeight();
404 				lineWidth = 0;
405 				++currentLineNumber;
406 				lineJustifications.push_back(currentState._justification);
407 			}
408 			if (stateChanges & TEXT_CHANGE_HAS_STATE_BOX) {
409 				Common::String temp = Common::String::format("%d", _engine->getScriptManager()->getStateValue(currentState._statebox));
410 				wordWidth += font.getStringWidth(temp);
411 
412 				// If the word causes the line to overflow, render the sentence and start a new line
413 				if (lineWidth + sentenceWidth + wordWidth > dest.w) {
414 					textSurfaces.push_back(TextSurface(font.renderSolidText(currentSentence, textColor), sentencePixelOffset, currentLineNumber));
415 
416 					// Set line width
417 					lineWidths.push_back(lineWidth + sentenceWidth - (numSpaces * spaceWidth));
418 
419 					currentSentence.clear();
420 					sentenceWidth = 0;
421 
422 					// Update the offsets
423 					sentencePixelOffset.x = 0u;
424 					sentencePixelOffset.y += lineHeight;
425 
426 					// Reset the line variables
427 					lineHeight = font.getFontHeight();
428 					lineWidth = 0;
429 					++currentLineNumber;
430 					lineJustifications.push_back(currentState._justification);
431 				}
432 			}
433 		} else {
434 			currentWord += text[i];
435 			wordWidth += font.getCharWidth(text[i]);
436 
437 			if (text[i] == ' ') {
438 				// When we hit the first space, flush the current word to the sentence
439 				if (!currentWord.empty()) {
440 					currentSentence += currentWord;
441 					sentenceWidth += wordWidth;
442 
443 					currentWord.clear();
444 					wordWidth = 0;
445 				}
446 
447 				// We track the number of spaces so we can disregard their width in lineWidth calculations
448 				++numSpaces;
449 			} else {
450 				// If the word causes the line to overflow, render the sentence and start a new line
451 				if (lineWidth + sentenceWidth + wordWidth > dest.w) {
452 					// Only render out content
453 					if (!currentSentence.empty()) {
454 						textSurfaces.push_back(TextSurface(font.renderSolidText(currentSentence, currentState.getTextColor(_engine)), sentencePixelOffset, currentLineNumber));
455 					}
456 
457 					// Set line width
458 					lineWidths.push_back(lineWidth + sentenceWidth - (numSpaces * spaceWidth));
459 
460 					currentSentence.clear();
461 					sentenceWidth = 0;
462 
463 					// Update the offsets
464 					sentencePixelOffset.x = 0u;
465 					sentencePixelOffset.y += lineHeight;
466 
467 					// Reset the line variables
468 					lineHeight = font.getFontHeight();
469 					lineWidth = 0;
470 					++currentLineNumber;
471 					lineJustifications.push_back(currentState._justification);
472 				}
473 
474 				numSpaces = 0u;
475 			}
476 		}
477 
478 		i++;
479 	}
480 
481 	// Render out any remaining words/sentences
482 	if (!currentWord.empty() || !currentSentence.empty()) {
483 		currentSentence += currentWord;
484 		sentenceWidth += wordWidth;
485 
486 		textSurfaces.push_back(TextSurface(font.renderSolidText(currentSentence, currentState.getTextColor(_engine)), sentencePixelOffset, currentLineNumber));
487 	}
488 
489 	lineWidths.push_back(lineWidth + sentenceWidth);
490 	lineJustifications.push_back(currentState._justification);
491 
492 	for (Common::Array<TextSurface>::iterator iter = textSurfaces.begin(); iter != textSurfaces.end(); ++iter) {
493 		Common::Rect empty;
494 
495 		if (lineJustifications[iter->_lineNumber] == TEXT_JUSTIFY_LEFT) {
496 			_engine->getRenderManager()->blitSurfaceToSurface(*iter->_surface, empty, dest, iter->_surfaceOffset.x, iter->_surfaceOffset.y, 0);
497 		} else if (lineJustifications[iter->_lineNumber] == TEXT_JUSTIFY_CENTER) {
498 			_engine->getRenderManager()->blitSurfaceToSurface(*iter->_surface, empty, dest, ((dest.w - lineWidths[iter->_lineNumber]) / 2) + iter->_surfaceOffset.x, iter->_surfaceOffset.y, 0);
499 		} else if (lineJustifications[iter->_lineNumber] == TEXT_JUSTIFY_RIGHT) {
500 			_engine->getRenderManager()->blitSurfaceToSurface(*iter->_surface, empty, dest, dest.w - lineWidths[iter->_lineNumber]  + iter->_surfaceOffset.x, iter->_surfaceOffset.y, 0);
501 		}
502 
503 		// Release memory
504 		iter->_surface->free();
505 		delete iter->_surface;
506 	}
507 }
508 
readWideLine(Common::SeekableReadStream & stream)509 Common::String readWideLine(Common::SeekableReadStream &stream) {
510 	Common::String asciiString;
511 
512 	while (true) {
513 		uint32 value = stream.readUint16LE();
514 		if (stream.eos())
515 			break;
516 		// Check for CRLF
517 		if (value == 0x0A0D) {
518 			// Read in the extra NULL char
519 			stream.readByte(); // \0
520 			// End of the line. Break
521 			break;
522 		}
523 
524 		// Crush each octet pair to a UTF-8 sequence
525 		if (value < 0x80) {
526 			asciiString += (char)(value & 0x7F);
527 		} else if (value >= 0x80 && value < 0x800) {
528 			asciiString += (char)(0xC0 | ((value >> 6) & 0x1F));
529 			asciiString += (char)(0x80 | (value & 0x3F));
530 		} else if (value >= 0x800 && value < 0x10000 && value != 0xCCCC) {
531 			asciiString += (char)(0xE0 | ((value >> 12) & 0xF));
532 			asciiString += (char)(0x80 | ((value >> 6) & 0x3F));
533 			asciiString += (char)(0x80 | (value & 0x3F));
534 		} else if (value == 0xCCCC) {
535 			// Ignore, this character is used as newline sometimes
536 		} else if (value >= 0x10000 && value < 0x200000) {
537 			asciiString += (char)(0xF0);
538 			asciiString += (char)(0x80 | ((value >> 12) & 0x3F));
539 			asciiString += (char)(0x80 | ((value >> 6) & 0x3F));
540 			asciiString += (char)(0x80 | (value & 0x3F));
541 		}
542 	}
543 
544 	return asciiString;
545 }
546 
getUtf8CharSize(char chr)547 int8 getUtf8CharSize(char chr) {
548 	if ((chr & 0x80) == 0)
549 		return 1;
550 	else if ((chr & 0xE0) == 0xC0)
551 		return 2;
552 	else if ((chr & 0xF0) == 0xE0)
553 		return 3;
554 	else if ((chr & 0xF8) == 0xF0)
555 		return 4;
556 	else if ((chr & 0xFC) == 0xF8)
557 		return 5;
558 	else if ((chr & 0xFE) == 0xFC)
559 		return 6;
560 
561 	return 1;
562 }
563 
readUtf8Char(const char * chr)564 uint16 readUtf8Char(const char *chr) {
565 	uint16 result = 0;
566 	if ((chr[0] & 0x80) == 0)
567 		result = chr[0];
568 	else if ((chr[0] & 0xE0) == 0xC0)
569 		result = ((chr[0] & 0x1F) << 6) | (chr[1] & 0x3F);
570 	else if ((chr[0] & 0xF0) == 0xE0)
571 		result = ((chr[0] & 0x0F) << 12) | ((chr[1] & 0x3F) << 6) | (chr[2] & 0x3F);
572 	else
573 		result = chr[0];
574 
575 	return result;
576 }
577 
578 } // End of namespace ZVision
579