1/*
2 * Copyright 2010, Stephan Aßmus <superstippi@gmx.de>.
3 * Distributed under the terms of the MIT License.
4 */
5
6
7#include "SubtitleBitmap.h"
8
9#include <stdio.h>
10
11#include <Bitmap.h>
12#include <TextView.h>
13
14#include "StackBlurFilter.h"
15
16
17SubtitleBitmap::SubtitleBitmap()
18	:
19	fBitmap(NULL),
20	fTextView(new BTextView("offscreen text")),
21	fShadowTextView(new BTextView("offscreen text shadow")),
22	fCharsPerLine(36),
23	fUseSoftShadow(true),
24	fOverlayMode(false)
25{
26	fTextView->SetStylable(true);
27	fTextView->MakeEditable(false);
28	fTextView->SetWordWrap(false);
29	fTextView->SetAlignment(B_ALIGN_CENTER);
30
31	fShadowTextView->SetStylable(true);
32	fShadowTextView->MakeEditable(false);
33	fShadowTextView->SetWordWrap(false);
34	fShadowTextView->SetAlignment(B_ALIGN_CENTER);
35}
36
37
38SubtitleBitmap::~SubtitleBitmap()
39{
40	delete fBitmap;
41	delete fTextView;
42	delete fShadowTextView;
43}
44
45
46bool
47SubtitleBitmap::SetText(const char* text)
48{
49	if (text == fText)
50		return false;
51
52	fText = text;
53
54	_GenerateBitmap();
55	return true;
56}
57
58
59void
60SubtitleBitmap::SetVideoBounds(BRect bounds)
61{
62	if (bounds == fVideoBounds)
63		return;
64
65	fVideoBounds = bounds;
66
67	fUseSoftShadow = true;
68	_GenerateBitmap();
69}
70
71
72void
73SubtitleBitmap::SetOverlayMode(bool overlayMode)
74{
75	if (overlayMode == fOverlayMode)
76		return;
77
78	fOverlayMode = overlayMode;
79
80	_GenerateBitmap();
81}
82
83
84void
85SubtitleBitmap::SetCharsPerLine(float charsPerLine)
86{
87	if (charsPerLine == fCharsPerLine)
88		return;
89
90	fCharsPerLine = charsPerLine;
91
92	fUseSoftShadow = true;
93	_GenerateBitmap();
94}
95
96
97const BBitmap*
98SubtitleBitmap::Bitmap() const
99{
100	return fBitmap;
101}
102
103
104void
105SubtitleBitmap::_GenerateBitmap()
106{
107	if (!fVideoBounds.IsValid())
108		return;
109
110	delete fBitmap;
111
112	BRect bounds;
113	float outlineRadius;
114	_InsertText(bounds, outlineRadius, fOverlayMode);
115
116	bigtime_t startTime = 0;
117	if (!fOverlayMode && fUseSoftShadow)
118		startTime = system_time();
119
120	fBitmap = new BBitmap(bounds, B_BITMAP_ACCEPTS_VIEWS, B_RGBA32);
121	memset(fBitmap->Bits(), 0, fBitmap->BitsLength());
122
123	if (fBitmap->Lock()) {
124		fBitmap->AddChild(fShadowTextView);
125		fShadowTextView->ResizeTo(bounds.Width(), bounds.Height());
126
127		fShadowTextView->SetViewColor(0, 0, 0, 0);
128		fShadowTextView->SetDrawingMode(B_OP_ALPHA);
129		fShadowTextView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);
130
131		fShadowTextView->PushState();
132		fShadowTextView->Draw(bounds);
133		fShadowTextView->PopState();
134
135		if (!fOverlayMode && fUseSoftShadow) {
136			fShadowTextView->Sync();
137			StackBlurFilter filter;
138			filter.Filter(fBitmap, outlineRadius * 2);
139		}
140
141		fShadowTextView->RemoveSelf();
142
143		fBitmap->AddChild(fTextView);
144		fTextView->ResizeTo(bounds.Width(), bounds.Height());
145		if (!fOverlayMode && fUseSoftShadow)
146			fTextView->MoveTo(-outlineRadius / 2, -outlineRadius / 2);
147		else
148			fTextView->MoveTo(0, 0);
149
150		fTextView->SetViewColor(0, 0, 0, 0);
151		fTextView->SetDrawingMode(B_OP_ALPHA);
152		fTextView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);
153
154		fTextView->PushState();
155		fTextView->Draw(bounds);
156		fTextView->PopState();
157
158		fTextView->Sync();
159		fTextView->RemoveSelf();
160
161		fBitmap->Unlock();
162	}
163
164	if (!fOverlayMode && fUseSoftShadow && system_time() - startTime > 10000)
165		fUseSoftShadow = false;
166}
167
168
169struct ParseState {
170	ParseState(rgb_color color)
171		:
172		color(color),
173		bold(false),
174		italic(false),
175		underlined(false),
176
177		previous(NULL)
178	{
179	}
180
181	ParseState(ParseState* previous)
182		:
183		color(previous->color),
184		bold(previous->bold),
185		italic(previous->italic),
186		underlined(previous->underlined),
187
188		previous(previous)
189	{
190	}
191
192	rgb_color	color;
193	bool		bold;
194	bool		italic;
195	bool		underlined;
196
197	ParseState*	previous;
198};
199
200
201static bool
202find_next_tag(const BString& string, int32& tagPos, int32& tagLength,
203	ParseState*& state)
204{
205	static const char* kTags[] = {
206		"<b>", "</b>",
207		"<i>", "</i>",
208		"<u>", "</u>",
209		"<font color=\"#", "</font>"
210	};
211	static const int32 kTagCount = sizeof(kTags) / sizeof(const char*);
212
213	int32 startPos = tagPos;
214	tagPos = string.Length();
215	tagLength = 0;
216
217	// Find the next tag closest from the current position
218	// This way of doing it allows broken input with overlapping tags even.
219	BString tag;
220	for (int32 i = 0; i < kTagCount; i++) {
221		int32 nextTag = string.IFindFirst(kTags[i], startPos);
222		if (nextTag >= startPos && nextTag < tagPos) {
223			tagPos = nextTag;
224			tag = kTags[i];
225		}
226	}
227
228	if (tag.Length() == 0)
229		return false;
230
231	// Tag found, ParseState will change.
232	tagLength = tag.Length();
233	if (tag == "<b>") {
234		state = new ParseState(state);
235		state->bold = true;
236	} else if (tag == "<i>") {
237		state = new ParseState(state);
238		state->italic = true;
239	} else if (tag == "<u>") {
240		state = new ParseState(state);
241		state->underlined = true;
242	} else if (tag == "<font color=\"#") {
243		state = new ParseState(state);
244		char number[16];
245		snprintf(number, sizeof(number), "0x%.6s",
246			string.String() + tagPos + tag.Length());
247		int colorInt;
248		if (sscanf(number, "%x", &colorInt) == 1) {
249			state->color.red = (colorInt & 0xff0000) >> 16;
250			state->color.green = (colorInt & 0x00ff00) >> 8;
251			state->color.blue = (colorInt & 0x0000ff);
252			// skip 'RRGGBB">' part, too
253			tagLength += 8;
254		}
255	} else if (tag == "</b>" || tag == "</i>" || tag == "</u>"
256		|| tag == "</font>") {
257		// Closing tag, pop state
258		if (state->previous != NULL) {
259			ParseState* oldState = state;
260			state = state->previous;
261			delete oldState;
262		}
263	}
264	return true;
265}
266
267
268static void
269apply_state(BTextView* textView, const ParseState* state, BFont font,
270	bool changeColor)
271{
272	uint16 face = 0;
273	if (state->bold || state->italic || state->underlined) {
274		if (state->bold)
275			face |= B_BOLD_FACE;
276		if (state->italic)
277			face |= B_ITALIC_FACE;
278		// NOTE: This is probably not supported by the app_server (perhaps
279		// it is if the font contains a specific underline face).
280		if (state->underlined)
281			face |= B_UNDERSCORE_FACE;
282	} else
283		face = B_REGULAR_FACE;
284	font.SetFace(face);
285	if (changeColor)
286		textView->SetFontAndColor(&font, B_FONT_ALL, &state->color);
287	else
288		textView->SetFontAndColor(&font, B_FONT_ALL, NULL);
289}
290
291
292static void
293parse_text(const BString& string, BTextView* textView, const BFont& font,
294	const rgb_color& color, bool changeColor)
295{
296	ParseState rootState(color);
297		// Colors may change, but alpha channel will be preserved
298
299	ParseState* state = &rootState;
300
301	int32 pos = 0;
302	while (pos < string.Length()) {
303		int32 nextPos = pos;
304		int32 tagLength;
305		bool stateChanged = find_next_tag(string, nextPos, tagLength, state);
306		if (nextPos > pos) {
307			// Insert text between last and next tags
308			BString subString;
309			string.CopyInto(subString, pos, nextPos - pos);
310			textView->Insert(subString.String());
311		}
312		pos = nextPos + tagLength;
313		if (stateChanged)
314			apply_state(textView, state, font, changeColor);
315	}
316
317	// Cleanup states in case the input text had non-matching tags.
318	while (state->previous != NULL) {
319		ParseState* oldState = state;
320		state = state->previous;
321		delete oldState;
322	}
323}
324
325
326void
327SubtitleBitmap::_InsertText(BRect& textRect, float& outlineRadius,
328	bool overlayMode)
329{
330	BFont font(be_plain_font);
331	float fontSize = ceilf((fVideoBounds.Width() * 0.9) / fCharsPerLine);
332	outlineRadius = ceilf(fontSize / 28.0);
333	font.SetSize(fontSize);
334
335	rgb_color shadow;
336	shadow.red = 0;
337	shadow.green = 0;
338	shadow.blue = 0;
339	shadow.alpha = 200;
340
341	rgb_color color;
342	color.red = 255;
343	color.green = 255;
344	color.blue = 255;
345	color.alpha = 240;
346
347	textRect = fVideoBounds;
348	textRect.OffsetBy(outlineRadius, outlineRadius);
349
350	fTextView->SetText(NULL);
351	fTextView->SetFontAndColor(&font, B_FONT_ALL, &color);
352
353	fTextView->Insert(" ");
354	parse_text(fText, fTextView, font, color, true);
355
356	font.SetFalseBoldWidth(outlineRadius);
357	fShadowTextView->ForceFontAliasing(overlayMode);
358	fShadowTextView->SetText(NULL);
359	fShadowTextView->SetFontAndColor(&font, B_FONT_ALL, &shadow);
360
361	fShadowTextView->Insert(" ");
362	parse_text(fText, fShadowTextView, font, shadow, false);
363
364	// This causes the BTextView to calculate the layout of the text
365	fTextView->SetTextRect(BRect(0, 0, 0, 0));
366	fTextView->SetTextRect(textRect);
367	fShadowTextView->SetTextRect(BRect(0, 0, 0, 0));
368	fShadowTextView->SetTextRect(textRect);
369
370	textRect = fTextView->TextRect();
371	textRect.InsetBy(-outlineRadius, -outlineRadius);
372	textRect.OffsetTo(B_ORIGIN);
373
374	// Make sure the text rect really finishes behind the last line.
375	// We don't want any accidental extra space.
376	textRect.bottom = outlineRadius;
377	int32 lineCount = fTextView->CountLines();
378	for (int32 i = 0; i < lineCount; i++)
379		textRect.bottom += fTextView->LineHeight(i);
380	textRect.bottom += outlineRadius;
381}
382
383
384
385