xref: /haiku/src/apps/mediaplayer/interface/SubtitleBitmap.cpp (revision 04171cfc5c10c98b9ba3c7233a271f6165cdd36f)
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 
17 SubtitleBitmap::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 
38 SubtitleBitmap::~SubtitleBitmap()
39 {
40 	delete fBitmap;
41 	delete fTextView;
42 	delete fShadowTextView;
43 }
44 
45 
46 bool
47 SubtitleBitmap::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 
59 void
60 SubtitleBitmap::SetVideoBounds(BRect bounds)
61 {
62 	if (bounds == fVideoBounds)
63 		return;
64 
65 	fVideoBounds = bounds;
66 
67 	fUseSoftShadow = true;
68 	_GenerateBitmap();
69 }
70 
71 
72 void
73 SubtitleBitmap::SetOverlayMode(bool overlayMode)
74 {
75 	if (overlayMode == fOverlayMode)
76 		return;
77 
78 	fOverlayMode = overlayMode;
79 
80 	_GenerateBitmap();
81 }
82 
83 
84 void
85 SubtitleBitmap::SetCharsPerLine(float charsPerLine)
86 {
87 	if (charsPerLine == fCharsPerLine)
88 		return;
89 
90 	fCharsPerLine = charsPerLine;
91 
92 	fUseSoftShadow = true;
93 	_GenerateBitmap();
94 }
95 
96 
97 const BBitmap*
98 SubtitleBitmap::Bitmap() const
99 {
100 	return fBitmap;
101 }
102 
103 
104 void
105 SubtitleBitmap::_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 
169 struct 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 
201 static bool
202 find_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 
268 static void
269 apply_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 
292 static void
293 parse_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 
326 void
327 SubtitleBitmap::_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