root/src/apps/mediaplayer/interface/SubtitleBitmap.cpp
/*
 * Copyright 2010, Stephan Aßmus <superstippi@gmx.de>.
 * Distributed under the terms of the MIT License.
 */


#include "SubtitleBitmap.h"

#include <stdio.h>

#include <Bitmap.h>
#include <TextView.h>

#include "StackBlurFilter.h"


SubtitleBitmap::SubtitleBitmap()
        :
        fBitmap(NULL),
        fTextView(new BTextView("offscreen text")),
        fShadowTextView(new BTextView("offscreen text shadow")),
        fCharsPerLine(36),
        fUseSoftShadow(true),
        fOverlayMode(false)
{
        fTextView->SetStylable(true);
        fTextView->MakeEditable(false);
        fTextView->SetAlignment(B_ALIGN_CENTER);

        fShadowTextView->SetStylable(true);
        fShadowTextView->MakeEditable(false);
        fShadowTextView->SetAlignment(B_ALIGN_CENTER);
}


SubtitleBitmap::~SubtitleBitmap()
{
        delete fBitmap;
        delete fTextView;
        delete fShadowTextView;
}


bool
SubtitleBitmap::SetText(const char* text)
{
        if (text == fText)
                return false;

        fText = text;

        _GenerateBitmap();
        return true;
}


void
SubtitleBitmap::SetVideoBounds(BRect bounds)
{
        if (bounds == fVideoBounds)
                return;

        fVideoBounds = bounds;

        fUseSoftShadow = true;
        _GenerateBitmap();
}


void
SubtitleBitmap::SetOverlayMode(bool overlayMode)
{
        if (overlayMode == fOverlayMode)
                return;

        fOverlayMode = overlayMode;

        _GenerateBitmap();
}


void
SubtitleBitmap::SetCharsPerLine(float charsPerLine)
{
        if (charsPerLine == fCharsPerLine)
                return;

        fCharsPerLine = charsPerLine;

        fUseSoftShadow = true;
        _GenerateBitmap();
}


const BBitmap*
SubtitleBitmap::Bitmap() const
{
        return fBitmap;
}


void
SubtitleBitmap::_GenerateBitmap()
{
        if (!fVideoBounds.IsValid())
                return;

        delete fBitmap;

        BRect bounds;
        float outlineRadius;
        _InsertText(bounds, outlineRadius, fOverlayMode);

        bigtime_t startTime = 0;
        if (!fOverlayMode && fUseSoftShadow)
                startTime = system_time();

        fBitmap = new BBitmap(bounds, B_BITMAP_ACCEPTS_VIEWS, B_RGBA32);
        memset(fBitmap->Bits(), 0, fBitmap->BitsLength());

        if (fBitmap->Lock()) {
                fBitmap->AddChild(fShadowTextView);
                fShadowTextView->ResizeTo(bounds.Width(), bounds.Height());

                fShadowTextView->SetViewColor(0, 0, 0, 0);
                fShadowTextView->SetDrawingMode(B_OP_ALPHA);
                fShadowTextView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);

                fShadowTextView->PushState();
                fShadowTextView->Draw(bounds);
                fShadowTextView->PopState();

                if (!fOverlayMode && fUseSoftShadow) {
                        fShadowTextView->Sync();
                        StackBlurFilter filter;
                        filter.Filter(fBitmap, outlineRadius * 2);
                }

                fShadowTextView->RemoveSelf();

                fBitmap->AddChild(fTextView);
                fTextView->ResizeTo(bounds.Width(), bounds.Height());
                if (!fOverlayMode && fUseSoftShadow)
                        fTextView->MoveTo(-outlineRadius / 2, -outlineRadius / 2);
                else
                        fTextView->MoveTo(0, 0);

                fTextView->SetViewColor(0, 0, 0, 0);
                fTextView->SetDrawingMode(B_OP_ALPHA);
                fTextView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);

                fTextView->PushState();
                fTextView->Draw(bounds);
                fTextView->PopState();

                fTextView->Sync();
                fTextView->RemoveSelf();

                fBitmap->Unlock();
        }

        if (!fOverlayMode && fUseSoftShadow && system_time() - startTime > 10000)
                fUseSoftShadow = false;
}


struct ParseState {
        ParseState(rgb_color color)
                :
                color(color),
                bold(false),
                italic(false),
                underlined(false),

                previous(NULL)
        {
        }

        ParseState(ParseState* previous)
                :
                color(previous->color),
                bold(previous->bold),
                italic(previous->italic),
                underlined(previous->underlined),

                previous(previous)
        {
        }

        rgb_color       color;
        bool            bold;
        bool            italic;
        bool            underlined;

        ParseState*     previous;
};


static bool
find_next_tag(const BString& string, int32& tagPos, int32& tagLength,
        ParseState*& state)
{
        static const char* kTags[] = {
                "<b>", "</b>",
                "<i>", "</i>",
                "<u>", "</u>",
                "<font color=\"#", "</font>"
        };
        static const int32 kTagCount = sizeof(kTags) / sizeof(const char*);

        int32 startPos = tagPos;
        tagPos = string.Length();
        tagLength = 0;

        // Find the next tag closest from the current position
        // This way of doing it allows broken input with overlapping tags even.
        BString tag;
        for (int32 i = 0; i < kTagCount; i++) {
                int32 nextTag = string.IFindFirst(kTags[i], startPos);
                if (nextTag >= startPos && nextTag < tagPos) {
                        tagPos = nextTag;
                        tag = kTags[i];
                }
        }

        if (tag.Length() == 0)
                return false;

        // Tag found, ParseState will change.
        tagLength = tag.Length();
        if (tag == "<b>") {
                state = new ParseState(state);
                state->bold = true;
        } else if (tag == "<i>") {
                state = new ParseState(state);
                state->italic = true;
        } else if (tag == "<u>") {
                state = new ParseState(state);
                state->underlined = true;
        } else if (tag == "<font color=\"#") {
                state = new ParseState(state);
                char number[16];
                snprintf(number, sizeof(number), "0x%.6s",
                        string.String() + tagPos + tag.Length());
                int colorInt;
                if (sscanf(number, "%x", &colorInt) == 1) {
                        state->color.red = (colorInt & 0xff0000) >> 16;
                        state->color.green = (colorInt & 0x00ff00) >> 8;
                        state->color.blue = (colorInt & 0x0000ff);
                        // skip 'RRGGBB">' part, too
                        tagLength += 8;
                }
        } else if (tag == "</b>" || tag == "</i>" || tag == "</u>"
                || tag == "</font>") {
                // Closing tag, pop state
                if (state->previous != NULL) {
                        ParseState* oldState = state;
                        state = state->previous;
                        delete oldState;
                }
        }
        return true;
}


static void
apply_state(BTextView* textView, const ParseState* state, BFont font,
        bool changeColor)
{
        uint16 face = 0;
        if (state->bold || state->italic || state->underlined) {
                if (state->bold)
                        face |= B_BOLD_FACE;
                if (state->italic)
                        face |= B_ITALIC_FACE;
                if (state->underlined)
                        face |= B_UNDERSCORE_FACE;
        } else
                face = B_REGULAR_FACE;
        font.SetFace(face);
        if (changeColor)
                textView->SetFontAndColor(&font, B_FONT_ALL, &state->color);
        else
                textView->SetFontAndColor(&font, B_FONT_ALL, NULL);
}


static void
parse_text(const BString& string, BTextView* textView, const BFont& font,
        const rgb_color& color, bool changeColor)
{
        ParseState rootState(color);
                // Colors may change, but alpha channel will be preserved

        ParseState* state = &rootState;

        int32 pos = 0;
        while (pos < string.Length()) {
                int32 nextPos = pos;
                int32 tagLength;
                bool stateChanged = find_next_tag(string, nextPos, tagLength, state);
                if (nextPos > pos) {
                        // Insert text between last and next tags
                        BString subString;
                        string.CopyInto(subString, pos, nextPos - pos);
                        textView->Insert(subString.String());
                }
                pos = nextPos + tagLength;
                if (stateChanged)
                        apply_state(textView, state, font, changeColor);
        }

        // Cleanup states in case the input text had non-matching tags.
        while (state->previous != NULL) {
                ParseState* oldState = state;
                state = state->previous;
                delete oldState;
        }
}


void
SubtitleBitmap::_InsertText(BRect& textRect, float& outlineRadius,
        bool overlayMode)
{
        BFont font(be_plain_font);
        float fontSize = ceilf((fVideoBounds.Width() * 0.9) / fCharsPerLine);
        outlineRadius = ceilf(fontSize / 28.0);
        font.SetSize(fontSize);

        rgb_color shadow;
        shadow.red = 0;
        shadow.green = 0;
        shadow.blue = 0;
        shadow.alpha = 200;

        rgb_color color;
        color.red = 255;
        color.green = 255;
        color.blue = 255;
        color.alpha = 240;

        textRect = fVideoBounds;
        textRect.OffsetBy(outlineRadius, outlineRadius);

        fTextView->SetText(NULL);
        fTextView->SetFontAndColor(&font, B_FONT_ALL, &color);
        fTextView->ResizeTo(fVideoBounds.Width(), fVideoBounds.Height());

        fTextView->Insert(" ");
        parse_text(fText, fTextView, font, color, true);

        font.SetFalseBoldWidth(outlineRadius);
        fShadowTextView->ForceFontAliasing(overlayMode);
        fShadowTextView->SetText(NULL);
        fShadowTextView->SetFontAndColor(&font, B_FONT_ALL, &shadow);
        fShadowTextView->ResizeTo(fVideoBounds.Width(), fVideoBounds.Height());

        fShadowTextView->Insert(" ");
        parse_text(fText, fShadowTextView, font, shadow, false);

        // This causes the BTextView to calculate the layout of the text
        fTextView->SetTextRect(BRect(0, 0, 0, 0));
        fTextView->SetTextRect(textRect);
        fShadowTextView->SetTextRect(BRect(0, 0, 0, 0));
        fShadowTextView->SetTextRect(textRect);

        textRect = fTextView->TextRect();
        textRect.InsetBy(-outlineRadius, -outlineRadius);
        textRect.OffsetTo(B_ORIGIN);

        // Make sure the text rect really finishes behind the last line.
        // We don't want any accidental extra space.
        textRect.bottom = outlineRadius;
        int32 lineCount = fTextView->CountLines();
        for (int32 i = 0; i < lineCount; i++)
                textRect.bottom += fTextView->LineHeight(i);
        textRect.bottom += outlineRadius;
}