1 /* GemRB - Infinity Engine Emulator
2  * Copyright (C) 2003 The GemRB Project
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8 
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13 
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  *
18  *
19  */
20 
21 #include "Font.h"
22 
23 #include "GameData.h"
24 #include "Interface.h"
25 #include "Palette.h"
26 #include "Video.h"
27 
28 #include <cwctype>
29 
30 namespace GemRB {
31 
BlitGlyphToCanvas(const Glyph & glyph,const Point & p,ieByte * canvas,const Size & size)32 static void BlitGlyphToCanvas(const Glyph& glyph, const Point& p,
33 							  ieByte* canvas, const Size& size)
34 {
35 	const ieByte* src = glyph.pixels;
36 	if (canvas == NULL || src == NULL) {
37 		return; // need both a src and dst
38 	}
39 
40 	// find the origin and clip to it.
41 	// only worry about origin < 0.
42 	Point blitPoint = p + glyph.pos;
43 	Size srcSize = glyph.size;
44 	if (blitPoint.y < 0) {
45 		int offset = (-blitPoint.y * glyph.size.w);
46 		src += offset;
47 		srcSize.h -= offset;
48 		blitPoint.y = 0;
49 	}
50 	if (blitPoint.x < 0) {
51 		int offset = -blitPoint.x;
52 		src += offset;
53 		srcSize.w -= offset;
54 		blitPoint.x = 0;
55 	}
56 	ieByte* dest = canvas + (size.w * blitPoint.y) + blitPoint.x;
57 	assert(src >= glyph.pixels);
58 	assert(dest >= canvas);
59 	// copy the glyph to the canvas
60 	for(int row = 0; row < srcSize.h; row++ ) {
61 		if (dest + srcSize.w > canvas + (size.w * size.h)) {
62 			break;
63 		}
64 		memcpy(dest, src, srcSize.w);
65 		dest += size.w;
66 		src += glyph.pitch;
67 	}
68 }
69 
GlyphAtlasPage(Size pageSize,Font * font)70 Font::GlyphAtlasPage::GlyphAtlasPage(Size pageSize, Font* font)
71 : SpriteSheet<ieWord>(core->GetVideoDriver()), font(font)
72 {
73 	pageXPos = 0;
74 	SheetRegion.w = pageSize.w;
75 	SheetRegion.h = pageSize.h;
76 
77 	pageData = (ieByte*)calloc(pageSize.h, pageSize.w);
78 }
79 
AddGlyph(ieWord chr,const Glyph & g)80 bool Font::GlyphAtlasPage::AddGlyph(ieWord chr, const Glyph& g)
81 {
82 	assert(glyphs.find(chr) == glyphs.end());
83 	int newX = pageXPos + g.size.w;
84 	if (newX > SheetRegion.w) {
85 		return false;
86 	}
87 
88 	ieByte* pixels = nullptr;
89 	int glyphH = g.size.h + abs(g.pos.y);
90 	if (glyphH > SheetRegion.h) {
91 		// must grow to accommodate this glyph
92 		if (Sheet) {
93 			// if we already have a sheet we need to destroy it before we can add more glyphs
94 			pageData = (ieByte*)calloc(SheetRegion.w, glyphH);
95 			const ieByte* pixels = static_cast<const ieByte*>(Sheet->LockSprite());
96 			std::copy(pixels, pixels + (Sheet->Frame.w * Sheet->Frame.h), pageData);
97 			Sheet->UnlockSprite();
98 			Sheet = nullptr;
99 		} else {
100 			pageData = (ieByte*)realloc(pageData, SheetRegion.w * glyphH);
101 		}
102 
103 		assert(pageData);
104 		SheetRegion.h = glyphH;
105 		pixels = pageData;
106 	} else if (Sheet) {
107 		// we need to lock/unlock the sprite because we are updating its pixels
108 		pixels = (ieByte*)Sheet->LockSprite();
109 		assert(pixels == pageData);
110 	} else {
111 		pixels = pageData;
112 	}
113 
114 	// have to adjust the x because BlitGlyphToCanvas will use g.pos.x, but we dont want that here.
115 	BlitGlyphToCanvas(g, Point(pageXPos - g.pos.x, (g.pos.y < 0) ? -g.pos.y : 0), pageData, SheetRegion.Dimensions());
116 	MapSheetSegment(chr, Region(pageXPos, (g.pos.y < 0) ? 0 : g.pos.y, g.size.w, g.size.h));
117 	// make the non-temporary glyph from our own data
118 	ieByte* pageLoc = pageData + pageXPos;
119 	glyphs.insert(std::make_pair(chr, Glyph(g.size, g.pos, pageLoc, SheetRegion.w)));
120 
121 	pageXPos = newX;
122 
123 	if (Sheet) {
124 		Sheet->UnlockSprite();
125 	}
126 
127 	return true;
128 }
129 
GlyphForChr(ieWord chr) const130 const Glyph& Font::GlyphAtlasPage::GlyphForChr(ieWord chr) const
131 {
132 	GlyphMap::const_iterator it = glyphs.find(chr);
133 	if (it != glyphs.end()) {
134 		return it->second;
135 	}
136 	const static Glyph blank(Size(0,0), Point(0, 0), NULL, 0);
137 	return blank;
138 }
139 
Draw(ieWord chr,const Region & dest,const PrintColors * colors)140 void Font::GlyphAtlasPage::Draw(ieWord chr, const Region& dest, const PrintColors* colors)
141 {
142 	// ensure that we have a sprite!
143 	if (Sheet == NULL) {
144 		//Sheet = core->GetVideoDriver()->CreateSprite8(SheetRegion.w, SheetRegion.h, pageData, pal, true, 0);
145 		Sheet = core->GetVideoDriver()->CreateSprite8(SheetRegion, pageData, font->palette, true, 0);
146 		if (font->background) {
147 			invertedSheet = Sheet->copy();
148 			auto invertedPalette = font->palette->Copy();
149 			for (auto& c : invertedPalette->col) {
150 				c.r = 255 - c.r;
151 				c.g = 255 - c.g;
152 				c.b = 255 - c.b;
153 			}
154 			invertedSheet->SetPalette(invertedPalette);
155 		}
156 	}
157 
158 	if (colors) {
159 		if (font->background) {
160 			SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::BLENDED | BlitFlags::COLOR_MOD, colors->bg);
161 			// no point in BlitFlags::ADD with black so let's optimize away some blits
162 			if (colors->fg != ColorBlack) {
163 				std::swap(Sheet, invertedSheet);
164 				SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::ADD | BlitFlags::COLOR_MOD, colors->fg);
165 				std::swap(Sheet, invertedSheet);
166 			}
167 		} else {
168 			SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::BLENDED | BlitFlags::COLOR_MOD, colors->fg);
169 		}
170 	} else {
171 		SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::BLENDED, ColorWhite);
172 	}
173 }
174 
DumpToScreen(const Region & r)175 void Font::GlyphAtlasPage::DumpToScreen(const Region& r)
176 {
177 	Video* video = core->GetVideoDriver();
178 	video->SetScreenClip(NULL);
179 	Region drawRgn = Region(0, 0, 1024, Sheet->Frame.h);
180 	video->DrawRect(drawRgn, ColorBlack, true);
181 	video->DrawRect(Sheet->Frame.Intersect(r), ColorWhite, false);
182 	video->BlitSprite(Sheet, Sheet->Frame.Intersect(r), drawRgn, BlitFlags::BLENDED);
183 }
184 
Font(PaletteHolder pal,ieWord lineheight,ieWord baseline,bool bg)185 Font::Font(PaletteHolder pal, ieWord lineheight, ieWord baseline, bool bg)
186 : palette(pal), LineHeight(lineheight), Baseline(baseline)
187 {
188 	CurrentAtlasPage = NULL;
189 	background = bg;
190 }
191 
~Font(void)192 Font::~Font(void)
193 {
194 	GlyphAtlas::iterator it;
195 	for (it = Atlas.begin(); it != Atlas.end(); ++it) {
196 		delete *it;
197 	}
198 }
199 
CreateGlyphIndex(ieWord chr,ieWord pageIdx,const Glyph * g)200 void Font::CreateGlyphIndex(ieWord chr, ieWord pageIdx, const Glyph* g)
201 {
202 	if (chr >= AtlasIndex.size()) {
203 		// potentially wasteful I guess, but much faster than a map.
204 		AtlasIndex.resize(chr+1);
205 	} else {
206 		assert(AtlasIndex[chr].pageIdx == static_cast<ieWord>(-1));
207 	}
208 	AtlasIndex[chr] = GlyphIndexEntry(chr, pageIdx, g);
209 }
210 
CreateGlyphForCharSprite(ieWord chr,const Holder<Sprite2D> spr)211 const Glyph& Font::CreateGlyphForCharSprite(ieWord chr, const Holder<Sprite2D> spr)
212 {
213 	assert(AtlasIndex.size() <= chr || AtlasIndex[chr].pageIdx == static_cast<ieWord>(-1));
214 	assert(spr);
215 
216 	Size size(spr->Frame.w, spr->Frame.h);
217 	// FIXME: should we adjust for spr->Frame.x too?
218 	Point pos(0, Baseline - spr->Frame.y);
219 
220 	Glyph tmp = Glyph(size, pos, (ieByte*)spr->LockSprite(), spr->Frame.w);
221 	spr->UnlockSprite(); // FIXME: this is assuming it is ok to hang onto to pixel buffer returned from LockSprite()
222 	// adjust the location for the glyph
223 	if (!CurrentAtlasPage || !CurrentAtlasPage->AddGlyph(chr, tmp)) {
224 		// page is full, make a new one
225 		CurrentAtlasPage = new GlyphAtlasPage(Size(1024, LineHeight), this);
226 		Atlas.push_back(CurrentAtlasPage);
227 		bool ok = CurrentAtlasPage->AddGlyph(chr, tmp);
228 		assert(ok);
229 	}
230 	assert(CurrentAtlasPage);
231 	const Glyph& g = CurrentAtlasPage->GlyphForChr(chr);
232 	CreateGlyphIndex(chr, Atlas.size() - 1, &g);
233 	return g;
234 }
235 
CreateAliasForChar(ieWord chr,ieWord alias)236 void Font::CreateAliasForChar(ieWord chr, ieWord alias)
237 {
238 	// we cannot create an alias for a character that doesnt exist
239 	assert(AtlasIndex.size() > chr && AtlasIndex[chr].pageIdx != static_cast<ieWord>(-1));
240 
241 	// we need to now find the page for the existing character and add this new one to that page
242 	const GlyphIndexEntry& idx = AtlasIndex[chr]; // this referenece may become invalid after call to CreateGlyphIndex!
243 	ieWord pageIdx = idx.pageIdx;
244 	CreateGlyphIndex(alias, pageIdx, idx.glyph);
245 	Atlas[pageIdx]->MapSheetSegment(alias, (*Atlas[pageIdx])[chr]);
246 }
247 
GetGlyph(ieWord chr) const248 const Glyph& Font::GetGlyph(ieWord chr) const
249 {
250 	if (chr < AtlasIndex.size()) {
251 		const Glyph* g = AtlasIndex[chr].glyph;
252 		if (g) {
253 			return *g;
254 		}
255 	}
256 	const static Glyph blank(Size(0,0), Point(0, 0), NULL, 0);
257 	return blank;
258 }
259 
RenderText(const String & string,Region & rgn,ieByte alignment,const PrintColors * colors,Point * point,ieByte ** canvas,bool grow) const260 size_t Font::RenderText(const String& string, Region& rgn, ieByte alignment, const PrintColors* colors,
261 						Point* point, ieByte** canvas, bool grow) const
262 {
263 	// NOTE: vertical alignment is not handled here.
264 	// it should have been calculated previously and passed in via the "point" parameter
265 
266 	bool singleLine = (alignment&IE_FONT_SINGLE_LINE);
267 	Point dp((point) ? point->x : 0, (point) ? point->y : 0);
268 	const Region& sclip = core->GetVideoDriver()->GetScreenClip();
269 
270 	size_t charCount = 0;
271 	bool lineBreak = false;
272 	size_t stringPos = 0;
273 	String line;
274 	while (lineBreak || stringPos < string.length()) {
275 		if (lineBreak) {
276 			lineBreak = false;
277 		} else {
278 			size_t eolpos = string.find_first_of(L'\n', stringPos);
279 			if (eolpos == String::npos) {
280 				eolpos = string.length();
281 			} else {
282 				eolpos++; // convert from index
283 			}
284 			line = string.substr(stringPos, eolpos - stringPos);
285 			stringPos = eolpos;
286 		}
287 
288 		// check if we need to extend the canvas
289 		if (canvas && grow && rgn.h < dp.y) {
290 			size_t pos = (stringPos < string.length()) ? stringPos : string.length() - 1;
291 			pos -= line.length();
292 			Size textSize = StringSize(string.substr(pos));
293 			ieWord numNewPixels = textSize.Area();
294 			ieWord lineArea = rgn.w * LineHeight;
295 			// round up
296 			ieWord numLines = 1 + ((numNewPixels - 1) / lineArea);
297 			// extend the region and canvas both
298 			size_t curpos = rgn.h * rgn.w;
299 			int vGrow = (numLines * LineHeight);
300 			rgn.h += vGrow;
301 
302 			if (core->InDebugMode(ID_FONTS)) {
303 				Log(MESSAGE, "Font", "Resizing canvas from %dx%d to %dx%d",
304 					rgn.w, rgn.h - vGrow, rgn.w, rgn.h);
305 			}
306 
307 			*canvas = (ieByte*)realloc(*canvas, rgn.w * rgn.h);
308 			assert(canvas);
309 			// fill the buffer with the color key, or the new area or we will get garbage in the areas we dont blit to
310 			memset(*canvas + curpos, 0, vGrow * rgn.w);
311 		}
312 
313 		dp.x = 0;
314 		size_t lineLen = line.length();
315 		if (lineLen) {
316 			const Region lineRgn(dp + rgn.Origin(), Size(rgn.w, LineHeight));
317 			StringSizeMetrics metrics = {lineRgn.Dimensions(), 0, 0, true};
318 			const Size lineSize = StringSize(line, &metrics);
319 			size_t linePos = metrics.numChars;
320 			Point linePoint;
321 
322 			// check to see if the line is on screen
323 			// TODO: technically we could be *even more* optimized by passing lineRgn, but this breaks dropcaps
324 			// this isn't a big deal ATM, because the big text containers do line-by-line layout
325 			if (!sclip.IntersectsRegion(rgn)) {
326 				// offscreen, optimize by bypassing RenderLine, we pre-calculated linePos above
327 				// alignment is completely irrelevant here since the width is the same for all alignments
328 				linePoint.x = lineSize.w;
329 			} else {
330 				// on screen
331 				if (alignment&(IE_FONT_ALIGN_CENTER|IE_FONT_ALIGN_RIGHT)) {
332 					linePoint.x += (rgn.w - lineSize.w); // this is right aligned, but we can adjust for center later on
333 					if (linePoint.x < 0) {
334 						linePos = String::npos;
335 						size_t prevPos = linePos;
336 						String word;
337 						while (linePoint.x < 0) {
338 							// yuck, this is not optimal. not sure of a better way.
339 							// we have to rewind, word by word, until X >= 0
340 							linePos = line.find_last_of(L' ', prevPos);
341 							if (linePos == String::npos) {
342 								if (core->InDebugMode(ID_FONTS)) {
343 									Log(MESSAGE, "Font", "Horizontal alignment invalidated for '%ls' due to insufficient width %d", line.c_str(), lineSize.w);
344 								}
345 								linePoint.x = 0;
346 								break;
347 							}
348 							// word should be the space + word for calculation purposes
349 							word = line.substr(linePos, (prevPos - linePos) + 1);
350 							linePoint.x += StringSize(word).w;
351 							prevPos = linePos - 1;
352 						}
353 					}
354 					if (alignment&IE_FONT_ALIGN_CENTER) {
355 						linePoint.x /= 2;
356 					}
357 				}
358 				if (core->InDebugMode(ID_FONTS)) {
359 					core->GetVideoDriver()->DrawRect(lineRgn, ColorGreen, false);
360 					core->GetVideoDriver()->DrawRect(Region(linePoint + lineRgn.Origin(),
361 												 Size(lineSize.w, LineHeight)), ColorWhite, false);
362 				}
363 				linePos = RenderLine(line, lineRgn, linePoint, colors, canvas);
364 			}
365 			if (linePos == 0) {
366 				break; // if linePos == 0 then we would loop till we are out of bounds so just stop here
367 			}
368 
369 			dp = dp + linePoint;
370 			if (linePos < line.length() - 1) {
371 				// ignore whitespace between current pos and next word, if any (we are wrapping... maybe)
372 				linePos = line.find_first_not_of(WHITESPACE_STRING, linePos);
373 				if (linePos == String::npos) {
374 					linePos = line.length() - 1; // newline char accounted for later
375 				} else {
376 					lineBreak = true;
377 					if (!singleLine) {
378 						// dont bother getting the next line if we arent going to print it
379 						line = line.substr(linePos);
380 					}
381 				}
382 			}
383 			charCount += linePos;
384 		}
385 		dp.y += LineHeight;
386 
387 		if (singleLine || dp.y >= rgn.h) {
388 			break;
389 		}
390 	}
391 
392 	// free the unused canvas area (if any)
393 	if (canvas) {
394 		int usedh = dp.y;
395 		if (usedh < rgn.h) {
396 			// this is more than just saving memory
397 			// vertical alignment will be off if we have extra space
398 			*canvas = (ieByte*)realloc(*canvas, rgn.w * usedh);
399 			rgn.h = usedh;
400 		}
401 	}
402 
403 	if (point) {
404 		// deal with possible trailing newline
405 		if (charCount > 0 && string[charCount - 1] == L'\n') {
406 			dp.y += LineHeight;
407 		}
408 		*point = Point(dp.x, dp.y - LineHeight);
409 	}
410 
411 	assert(charCount <= string.length());
412 	return charCount;
413 }
414 
RenderLine(const String & line,const Region & lineRgn,Point & dp,const PrintColors * colors,ieByte ** canvas) const415 size_t Font::RenderLine(const String& line, const Region& lineRgn,
416 						Point& dp, const PrintColors* colors, ieByte** canvas) const
417 {
418 	assert(lineRgn.h == LineHeight);
419 
420 	// NOTE: alignment is not handled here.
421 	// it should have been calculated previously and passed in via the "point" parameter
422 
423 	size_t linePos = 0, wordBreak = 0;
424 
425 	// FIXME: I'm not sure how to handle Asian text
426 	// should a "word" be a single Asian glyph? that way we wouldnt clip off text (we were doing this before the rewrite too).
427 	// we could check the core encoding for the 'zerospace' attribute and treat single characters as words
428 	// that would looks funny with partial translations, however. we would need to handle both simultaniously.
429 
430 	// TODO: word breaks should probably happen on other characters such as '-' too.
431 	// not as simple as adding it to find_first_of
432 	bool done = false;
433 	do {
434 		wordBreak = line.find_first_of(L' ', linePos);
435 		String word;
436 		if (wordBreak == linePos) {
437 			word = L' ';
438 		} else {
439 			word = line.substr(linePos, wordBreak - linePos);
440 		}
441 
442 		StringSizeMetrics metrics = {lineRgn.Dimensions(), 0, 0, true};
443 		int wordW = StringSize(word, &metrics).w;
444 		if (dp.x == 0 && metrics.forceBreak) {
445 			done = true;
446 			word.resize(metrics.numChars);
447 			assert(metrics.size.w <= lineRgn.w);
448 		} else if (dp.x + wordW > lineRgn.w) {
449 			// overflow with no wrap allowed; abort.
450 			break;
451 		}
452 
453 		// print the word
454 		wchar_t currChar = '\0';
455 		size_t i = 0;
456 		for (; i < word.length(); i++) {
457 			// process glyphs in word
458 			currChar = word[i];
459 			if (currChar == '\r' || currChar == '\n') {
460 				continue;
461 			}
462 			if (i > 0) { // kerning
463 				dp.x -= GetKerningOffset(word[i-1], currChar);
464 			}
465 
466 			const Glyph& curGlyph = GetGlyph(currChar);
467 			Point blitPoint = dp + lineRgn.Origin() + curGlyph.pos;
468 			// use intersection because some rare glyphs can sometimes overlap lines
469 			if (!lineRgn.IntersectsRegion(Region(blitPoint, curGlyph.size))) {
470 				if (core->InDebugMode(ID_FONTS)) {
471 					core->GetVideoDriver()->DrawRect(lineRgn, ColorRed, false);
472 				}
473 				assert(metrics.forceBreak == false || dp.x > 0);
474 				done = true;
475 				break;
476 			}
477 
478 			if (canvas) {
479 				BlitGlyphToCanvas(curGlyph, blitPoint, *canvas, lineRgn.Dimensions());
480 			} else {
481 				size_t pageIdx = AtlasIndex[currChar].pageIdx;
482 				GlyphAtlasPage* page = Atlas[pageIdx];
483 				page->Draw(currChar, Region(blitPoint, curGlyph.size), colors);
484 			}
485 			dp.x += curGlyph.size.w;
486 		}
487 		linePos += i;
488 		if (done) break;
489 	} while (linePos < line.length());
490 	assert(linePos <= line.length());
491 	return linePos;
492 }
493 
RenderTextAsSprite(const String & string,const Size & size,ieByte alignment,size_t * numPrinted,Point * endPoint) const494 Holder<Sprite2D> Font::RenderTextAsSprite(const String& string, const Size& size,
495 								   ieByte alignment, size_t* numPrinted, Point* endPoint) const
496 {
497 	Size canvasSize = StringSize(string); // same as size(0, 0)
498 	// if the string is larger than the region shrink the canvas
499 	// except 0 means we should size to fit in that dimension
500 	if (size.w) {
501 		// potentially resize
502 		if (size.w < canvasSize.w) {
503 			if (!(alignment&IE_FONT_SINGLE_LINE)) {
504 				// we need to resize horizontally which creates new lines
505 				ieWord trimmedArea = ((canvasSize.w - size.w) * canvasSize.h);
506 				// this automatically becomes multiline, therefore use LineHeight
507 				ieWord lineArea = size.w * LineHeight;
508 				// round up
509 				ieWord numLines = 1 + ((trimmedArea - 1) / lineArea);
510 				if (!size.h) {
511 					// grow as much as needed vertically.
512 					canvasSize.h += (numLines * LineHeight);
513 					// there is a chance we didn't grow enough vertically...
514 					// we can't possibly know how lines will break ahead of time,
515 					// over a long enough paragraph we can overflow the canvas
516 					// this is handled in RenderText() by reallocing the canvas based on
517 					// the same estimation algorithim (total area of text) used here
518 				} else if (size.h > canvasSize.h) {
519 					// grow by line increments until we hit the limit
520 					// round up, because even a partial line should be blitted (and clipped)
521 					ieWord maxLines = 1 + (((size.h - canvasSize.h) - 1) / LineHeight);
522 					if (maxLines < numLines) {
523 						numLines = maxLines;
524 					}
525 					canvasSize.h += (numLines * LineHeight);
526 					// if the new canvas size is taller than size.h it will be dealt with later
527 				}
528 			}
529 			canvasSize.w = size.w;
530 		} else if (alignment&(IE_FONT_ALIGN_CENTER|IE_FONT_ALIGN_RIGHT)) {
531 			// the size width is how we center or right align so we cant trim the canvas
532 			canvasSize.w = size.w;
533 		}
534 		// else: we already fit in the designated area (horizontally). No need to resize.
535 	}
536 	if (canvasSize.h < LineHeight) {
537 		// should be at least LineHeight
538 		canvasSize.h = LineHeight;
539 	}
540 	if (size.h && size.h < canvasSize.h) {
541 		// we can't unbreak lines ("\n") so at best we can clip the text.
542 		canvasSize.h = size.h;
543 	}
544 	assert(size.h || canvasSize.h >= LineHeight);
545 
546 	// we must calloc because not all glyphs are equal height. set remainder to the color key
547 	ieByte* canvasPx = (ieByte*)calloc(canvasSize.w, canvasSize.h);
548 
549 	Region rgn = Region(Point(0,0), canvasSize);
550 	size_t ret = RenderText(string, rgn, alignment, nullptr, endPoint, &canvasPx, (size.h) ? false : true);
551 	if (numPrinted) {
552 		*numPrinted = ret;
553 	}
554 
555 	// must ue rgn! the canvas height might be changed in RenderText()
556 	if (alignment&IE_FONT_ALIGN_CENTER) {
557 		rgn.x = (size.w - rgn.w) / 2;
558 	} else if (alignment&IE_FONT_ALIGN_RIGHT) {
559 		rgn.x = size.w - rgn.w;
560 	}
561 	if (alignment&IE_FONT_ALIGN_MIDDLE) {
562 		rgn.y = -(size.h - rgn.h) / 2;
563 	} else if (alignment&IE_FONT_ALIGN_BOTTOM) {
564 		rgn.y = -(size.h - rgn.h);
565 	}
566 	Holder<Sprite2D> canvas = core->GetVideoDriver()->CreateSprite8(rgn, canvasPx, palette, true, 0);
567 
568 	return canvas;
569 }
570 
Print(const Region & rgn,const String & string,ieByte alignment,Point * point) const571 size_t Font::Print(const Region& rgn, const String& string, ieByte alignment, Point* point) const
572 {
573 	return Print(rgn, string, alignment, nullptr, point);
574 }
575 
Print(const Region & rgn,const String & string,ieByte alignment,const PrintColors & colors,Point * point) const576 size_t Font::Print(const Region& rgn, const String& string, ieByte alignment, const PrintColors& colors, Point* point) const
577 {
578 	return Print(rgn, string, alignment, &colors, point);
579 }
580 
Print(Region rgn,const String & string,ieByte alignment,const PrintColors * colors,Point * point) const581 size_t Font::Print(Region rgn, const String& string, ieByte alignment, const PrintColors* colors, Point* point) const
582 {
583 	if (rgn.Dimensions().IsEmpty()) return 0;
584 
585 	Point p = (point) ? *point : Point();
586 	if (alignment&(IE_FONT_ALIGN_MIDDLE|IE_FONT_ALIGN_BOTTOM)) {
587 		// we assume that point will be an offset from midde/bottom position
588 		Size stringSize;
589 		if (alignment&IE_FONT_SINGLE_LINE) {
590 			// we can optimize single lines without StringSize()
591 			stringSize.h = LineHeight;
592 		} else {
593 			stringSize = rgn.Dimensions();
594 			StringSizeMetrics metrics = {stringSize, 0, 0, true};
595 			stringSize = StringSize(string, &metrics);
596 			if (alignment&IE_FONT_NO_CALC && metrics.numChars < string.length()) {
597 				// PST GUISTORE, not sure what else
598 				stringSize.h = rgn.h;
599 			}
600 		}
601 
602 		// important: we must do this adjustment even if it leads to -p.y!
603 		// some labels depend on this behavior (BG2 GUIINV) :/
604 		if (alignment&IE_FONT_ALIGN_MIDDLE) {
605 			p.y += (rgn.h - stringSize.h) / 2;
606 		} else { // bottom alignment
607 			p.y += rgn.h - stringSize.h;
608 		}
609 	}
610 
611 	size_t ret = RenderText(string, rgn, alignment, colors, &p);
612 
613 	if (point) {
614 		*point = p;
615 	}
616 	return ret;
617 }
618 
StringSizeWidth(const String & string,size_t width,size_t * numChars) const619 size_t Font::StringSizeWidth(const String& string, size_t width, size_t* numChars) const
620 {
621 	size_t size = 0, i = 0;
622 	for (; i < string.length(); ++i) {
623 		wchar_t c = string[i];
624 		if (c == L'\n') {
625 			break;
626 		}
627 
628 		const Glyph& curGlyph = GetGlyph(c);
629 		ieWord chrW = curGlyph.size.w;
630 		if (i > 0) {
631 			chrW -= GetKerningOffset(string[i-1], string[i]);
632 		}
633 
634 		if (width > 0 && size + chrW >= width) {
635 			break;
636 		}
637 
638 		size += chrW;
639 	}
640 
641 	if (numChars) {
642 		*numChars = i;
643 	}
644 	return size;
645 }
646 
StringSize(const String & string,StringSizeMetrics * metrics) const647 Size Font::StringSize(const String& string, StringSizeMetrics* metrics) const
648 {
649 	if (!string.length()) return Size();
650 #define WILL_WRAP(val) \
651 	(stop && stop->w && lineW + val > stop->w)
652 
653 #define APPEND_TO_LINE(val) \
654 	lineW += val; charCount = i + 1; val = 0;
655 
656 	ieWord w = 0, lines = 1;
657 	ieWord lineW = 0, wordW = 0, spaceW = 0;
658 	bool newline = false, eos = false, ws = false, forceBreak = false;
659 	size_t i = 0, charCount = 0;
660 	const Size* stop = (metrics) ? &metrics->size : NULL;
661 	for (; i < string.length(); i++) {
662 		const Glyph& curGlyph = GetGlyph(string[i]);
663 		eos = (i == string.length() - 1);
664 		ws = std::iswspace(string[i]);
665 		if (!ws) {
666 			ieWord chrW = curGlyph.size.w;
667 			if (lineW > 0) { // kerning
668 				chrW -= GetKerningOffset(string[i-1], string[i]);
669 			}
670 			if (lineW == 0 && wordW > 0 && WILL_WRAP(wordW + chrW) && metrics->forceBreak && wordW <= stop->w) {
671 				// the word is longer than the line allows, but we allow a break mid-word
672 				forceBreak = true;
673 				newline = true;
674 				APPEND_TO_LINE(wordW);
675 			}
676 			wordW += chrW;
677 			// spaceW is the *cumulative* whitespace between the 2 words
678 			wordW += spaceW;
679 			spaceW = 0;
680 		} // no else
681 		if (ws || eos) {
682 			if (WILL_WRAP(spaceW + wordW)) {
683 				newline = true;
684 			} else {
685 				if (string[i] == L'\n') {
686 					// always append *everything* if there is \n
687 					lineW += spaceW; // everything else appended later
688 					newline = true;
689 				} else if (ws && string[i] != L'\r') {
690 					spaceW += curGlyph.size.w;
691 				}
692 				APPEND_TO_LINE(wordW);
693 			}
694 		}
695 
696 		if (newline || eos) {
697 			w = std::max(w, lineW);
698 			if (stop) {
699 				if (stop->h && (LineHeight * (lines + 1)) > stop->h ) {
700 					break;
701 				}
702 				if (eos && stop->w && wordW < stop->w ) {
703 					// its possible the last word of string is longer than any of the previous lines
704 					w = std::max(w, wordW);
705 				}
706 			} else {
707 				w = std::max(w, wordW);
708 			}
709 
710 			if (metrics && metrics->numLines > 0 && metrics->numLines <= lines) {
711 				break;
712 			}
713 
714 			if (newline) {
715 				newline = false;
716 				lines++;
717 				lineW = 0;
718 				spaceW = 0;
719 			}
720 		}
721 	}
722 #undef WILL_WRAP
723 #undef APPEND_TO_LINE
724 
725 	w += ((w == 0 && wordW == 0) || !newline) ? spaceW : 0;
726 
727 	if (metrics) {
728 		if (forceBreak) charCount--;
729 		metrics->forceBreak = forceBreak;
730 		metrics->numChars = charCount;
731 		metrics->size = Size(w, (LineHeight * lines));
732 		metrics->numLines = lines;
733 		if (core->InDebugMode(ID_FONTS)) {
734 			assert(metrics->numChars <= string.length());
735 			assert(w <= stop->w);
736 		}
737 		return metrics->size;
738 	}
739 
740 	return Size(w, (LineHeight * lines));
741 }
742 
743 }
744