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