* Added basic support for SRT subtitle files. It only works
if the SRT file is placed alongside the current playlist item under the same name (sans extension). The name of the language is taken from the file which needs to be separated by a dot (should be improved). * Instead of the black outline, subtitles have a nice drop shadow now, which is easier on the eyes somehow. git-svn-id: file:///srv/svn/repos/haiku/haiku/trunk@38827 a95241bf-73f2-0310-859d-f6bbb57e9c96
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
#include "ProxyAudioSupplier.h"
|
||||
#include "ProxyVideoSupplier.h"
|
||||
#include "TrackSupplier.h"
|
||||
#include "SubTitles.h"
|
||||
#include "VideoTrackSupplier.h"
|
||||
|
||||
using std::nothrow;
|
||||
@@ -72,6 +73,7 @@ void Controller::Listener::FileFinished() {}
|
||||
void Controller::Listener::FileChanged(PlaylistItem* item, status_t result) {}
|
||||
void Controller::Listener::VideoTrackChanged(int32) {}
|
||||
void Controller::Listener::AudioTrackChanged(int32) {}
|
||||
void Controller::Listener::SubTitleTrackChanged(int32) {}
|
||||
void Controller::Listener::VideoStatsChanged() {}
|
||||
void Controller::Listener::AudioStatsChanged() {}
|
||||
void Controller::Listener::PlaybackStateChanged(uint32) {}
|
||||
@@ -104,6 +106,8 @@ Controller::Controller()
|
||||
fAudioSupplier(new ProxyAudioSupplier(this)),
|
||||
fVideoTrackSupplier(NULL),
|
||||
fAudioTrackSupplier(NULL),
|
||||
fSubTitles(NULL),
|
||||
fSubTitlesIndex(-1),
|
||||
|
||||
fCurrentFrame(0),
|
||||
fDuration(0),
|
||||
@@ -248,6 +252,8 @@ Controller::SetTo(const PlaylistItemRef& item)
|
||||
|
||||
fVideoTrackSupplier = NULL;
|
||||
fAudioTrackSupplier = NULL;
|
||||
fSubTitles = NULL;
|
||||
fSubTitlesIndex = -1;
|
||||
|
||||
fCurrentFrame = 0;
|
||||
fDuration = 0;
|
||||
@@ -433,6 +439,17 @@ Controller::VideoTrackCount()
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
Controller::SubTitleTrackCount()
|
||||
{
|
||||
BAutolock _(this);
|
||||
|
||||
if (fTrackSupplier != NULL)
|
||||
return fTrackSupplier->CountSubTitleTracks();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
Controller::SelectAudioTrack(int n)
|
||||
{
|
||||
@@ -527,6 +544,58 @@ Controller::CurrentVideoTrack()
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
Controller::SelectSubTitleTrack(int n)
|
||||
{
|
||||
BAutolock _(this);
|
||||
|
||||
if (fTrackSupplier == NULL)
|
||||
return B_NO_INIT;
|
||||
|
||||
fSubTitlesIndex = n;
|
||||
fSubTitles = fTrackSupplier->SubTitleTrackForIndex(n);
|
||||
|
||||
const SubTitle* subTitle = NULL;
|
||||
if (fSubTitles != NULL)
|
||||
subTitle = fSubTitles->SubTitleAt(_TimePosition());
|
||||
if (subTitle != NULL)
|
||||
fVideoView->SetSubTitle(subTitle->text.String());
|
||||
else
|
||||
fVideoView->SetSubTitle(NULL);
|
||||
|
||||
_NotifySubTitleTrackChanged(n);
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
Controller::CurrentSubTitleTrack()
|
||||
{
|
||||
BAutolock _(this);
|
||||
|
||||
if (fSubTitles == NULL)
|
||||
return -1;
|
||||
|
||||
return fSubTitlesIndex;
|
||||
}
|
||||
|
||||
|
||||
const char*
|
||||
Controller::SubTitleTrackName(int n)
|
||||
{
|
||||
BAutolock _(this);
|
||||
|
||||
if (fTrackSupplier == NULL)
|
||||
return NULL;
|
||||
|
||||
const SubTitles* subTitles = fTrackSupplier->SubTitleTrackForIndex(n);
|
||||
if (subTitles == NULL)
|
||||
return NULL;
|
||||
|
||||
return subTitles->Name();
|
||||
}
|
||||
|
||||
|
||||
// #pragma mark -
|
||||
|
||||
|
||||
@@ -1014,6 +1083,18 @@ Controller::_NotifyAudioTrackChanged(int32 index) const
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Controller::_NotifySubTitleTrackChanged(int32 index) const
|
||||
{
|
||||
BList listeners(fListeners);
|
||||
int32 count = listeners.CountItems();
|
||||
for (int32 i = 0; i < count; i++) {
|
||||
Listener* listener = (Listener*)listeners.ItemAtFast(i);
|
||||
listener->SubTitleTrackChanged(index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Controller::_NotifyVideoStatsChanged() const
|
||||
{
|
||||
@@ -1136,7 +1217,16 @@ void
|
||||
Controller::NotifyCurrentFrameChanged(int64 frame) const
|
||||
{
|
||||
fCurrentFrame = frame;
|
||||
_NotifyPositionChanged((float)_TimePosition() / fDuration);
|
||||
bigtime_t timePosition = _TimePosition();
|
||||
_NotifyPositionChanged((float)timePosition / fDuration);
|
||||
|
||||
if (fSubTitles != NULL) {
|
||||
const SubTitle* subTitle = fSubTitles->SubTitleAt(timePosition);
|
||||
if (subTitle != NULL)
|
||||
fVideoView->SetSubTitle(subTitle->text.String());
|
||||
else
|
||||
fVideoView->SetSubTitle(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class TrackSupplier;
|
||||
class PlaylistItem;
|
||||
class ProxyAudioSupplier;
|
||||
class ProxyVideoSupplier;
|
||||
class SubTitles;
|
||||
class VideoTrackSupplier;
|
||||
class VideoView;
|
||||
|
||||
@@ -57,6 +58,7 @@ public:
|
||||
|
||||
virtual void VideoTrackChanged(int32 index);
|
||||
virtual void AudioTrackChanged(int32 index);
|
||||
virtual void SubTitleTrackChanged(int32 index);
|
||||
|
||||
virtual void VideoStatsChanged();
|
||||
virtual void AudioStatsChanged();
|
||||
@@ -93,12 +95,16 @@ public:
|
||||
|
||||
int AudioTrackCount();
|
||||
int VideoTrackCount();
|
||||
int SubTitleTrackCount();
|
||||
|
||||
status_t SelectAudioTrack(int n);
|
||||
int CurrentAudioTrack();
|
||||
int AudioTrackChannelCount();
|
||||
status_t SelectVideoTrack(int n);
|
||||
int CurrentVideoTrack();
|
||||
status_t SelectSubTitleTrack(int n);
|
||||
int CurrentSubTitleTrack();
|
||||
const char* SubTitleTrackName(int n);
|
||||
|
||||
void Stop();
|
||||
void Play();
|
||||
@@ -158,6 +164,7 @@ private:
|
||||
void _NotifyFileFinished() const;
|
||||
void _NotifyVideoTrackChanged(int32 index) const;
|
||||
void _NotifyAudioTrackChanged(int32 index) const;
|
||||
void _NotifySubTitleTrackChanged(int32 index) const;
|
||||
|
||||
void _NotifyVideoStatsChanged() const;
|
||||
void _NotifyAudioStatsChanged() const;
|
||||
@@ -196,6 +203,8 @@ private:
|
||||
ProxyAudioSupplier* fAudioSupplier;
|
||||
VideoTrackSupplier* fVideoTrackSupplier;
|
||||
AudioTrackSupplier* fAudioTrackSupplier;
|
||||
const SubTitles* fSubTitles;
|
||||
int32 fSubTitlesIndex;
|
||||
|
||||
mutable int64 fCurrentFrame;
|
||||
bigtime_t fDuration;
|
||||
|
||||
@@ -83,6 +83,19 @@ ControllerObserver::AudioTrackChanged(int32 index)
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ControllerObserver::SubTitleTrackChanged(int32 index)
|
||||
{
|
||||
if (!(fObserveFlags & OBSERVE_TRACK_CHANGES))
|
||||
return;
|
||||
|
||||
BMessage message(MSG_CONTROLLER_SUB_TITLE_TRACK_CHANGED);
|
||||
message.AddInt32("index", index);
|
||||
|
||||
DeliverMessage(message);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ControllerObserver::VideoStatsChanged()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ enum {
|
||||
|
||||
MSG_CONTROLLER_VIDEO_TRACK_CHANGED = 'cnvt',
|
||||
MSG_CONTROLLER_AUDIO_TRACK_CHANGED = 'cnat',
|
||||
MSG_CONTROLLER_SUB_TITLE_TRACK_CHANGED = 'cnst',
|
||||
|
||||
MSG_CONTROLLER_VIDEO_STATS_CHANGED = 'cnvs',
|
||||
MSG_CONTROLLER_AUDIO_STATS_CHANGED = 'cnas',
|
||||
@@ -55,6 +56,7 @@ public:
|
||||
|
||||
virtual void VideoTrackChanged(int32 index);
|
||||
virtual void AudioTrackChanged(int32 index);
|
||||
virtual void SubTitleTrackChanged(int32 index);
|
||||
|
||||
virtual void VideoStatsChanged();
|
||||
virtual void AudioStatsChanged();
|
||||
|
||||
@@ -3,6 +3,7 @@ SubDir HAIKU_TOP src apps mediaplayer ;
|
||||
# for BRecentItems
|
||||
UsePublicHeaders [ FDirName be_apps Tracker ] ;
|
||||
UsePrivateHeaders interface shared ;
|
||||
UseLibraryHeaders agg ;
|
||||
|
||||
# source directories
|
||||
local sourceDirs =
|
||||
@@ -82,6 +83,8 @@ Application MediaPlayer :
|
||||
ProxyAudioSupplier.cpp
|
||||
ProxyVideoSupplier.cpp
|
||||
TrackSupplier.cpp
|
||||
SubTitles.cpp
|
||||
SubTitlesSRT.cpp
|
||||
VideoTrackSupplier.cpp
|
||||
|
||||
# support
|
||||
@@ -99,6 +102,7 @@ Application MediaPlayer :
|
||||
Notifier.cpp
|
||||
RWLocker.cpp
|
||||
SettingsMessage.cpp
|
||||
StackBlurFilter.cpp
|
||||
|
||||
# .
|
||||
Controller.cpp
|
||||
|
||||
@@ -95,10 +95,12 @@ enum {
|
||||
M_ASPECT_37_20,
|
||||
M_ASPECT_47_20,
|
||||
|
||||
M_SELECT_AUDIO_TRACK = 0x00000800,
|
||||
M_SELECT_AUDIO_TRACK_END = 0x00000fff,
|
||||
M_SELECT_VIDEO_TRACK = 0x00010000,
|
||||
M_SELECT_VIDEO_TRACK_END = 0x000fffff,
|
||||
M_SELECT_AUDIO_TRACK = 0x00000800,
|
||||
M_SELECT_AUDIO_TRACK_END = 0x00000fff,
|
||||
M_SELECT_VIDEO_TRACK = 0x00010000,
|
||||
M_SELECT_VIDEO_TRACK_END = 0x00010fff,
|
||||
M_SELECT_SUB_TITLE_TRACK = 0x00020000,
|
||||
M_SELECT_SUB_TITLE_TRACK_END = 0x00020fff,
|
||||
|
||||
M_SET_RATING,
|
||||
|
||||
@@ -680,6 +682,22 @@ MainWin::MessageReceived(BMessage* msg)
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MSG_CONTROLLER_SUB_TITLE_TRACK_CHANGED:
|
||||
{
|
||||
int32 index;
|
||||
if (msg->FindInt32("index", &index) == B_OK) {
|
||||
int32 i = 0;
|
||||
while (BMenuItem* item = fSubTitleTrackMenu->ItemAt(i)) {
|
||||
BMessage* message = item->Message();
|
||||
if (message != NULL) {
|
||||
item->SetMarked((int32)message->what
|
||||
- M_SELECT_SUB_TITLE_TRACK == index);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MSG_CONTROLLER_PLAYBACK_STATE_CHANGED:
|
||||
{
|
||||
uint32 state;
|
||||
@@ -971,6 +989,12 @@ MainWin::MessageReceived(BMessage* msg)
|
||||
fController->SelectVideoTrack(msg->what - M_SELECT_VIDEO_TRACK);
|
||||
break;
|
||||
}
|
||||
if ((int32)msg->what >= M_SELECT_SUB_TITLE_TRACK - 1
|
||||
&& msg->what <= M_SELECT_SUB_TITLE_TRACK_END) {
|
||||
fController->SelectSubTitleTrack((int32)msg->what
|
||||
- M_SELECT_SUB_TITLE_TRACK);
|
||||
break;
|
||||
}
|
||||
// let BWindow handle the rest
|
||||
BWindow::MessageReceived(msg);
|
||||
}
|
||||
@@ -1329,7 +1353,7 @@ MainWin::_SetupWindow()
|
||||
{
|
||||
// printf("MainWin::_SetupWindow\n");
|
||||
// Populate the track menus
|
||||
_SetupTrackMenus(fAudioTrackMenu, fVideoTrackMenu);
|
||||
_SetupTrackMenus(fAudioTrackMenu, fVideoTrackMenu, fSubTitleTrackMenu);
|
||||
_UpdateAudioChannelCount(fController->CurrentAudioTrack());
|
||||
|
||||
fVideoMenu->SetEnabled(fHasVideo);
|
||||
@@ -1382,6 +1406,7 @@ MainWin::_CreateMenu()
|
||||
fVideoAspectMenu = new BMenu("Aspect ratio");
|
||||
fAudioTrackMenu = new BMenu("Track");
|
||||
fVideoTrackMenu = new BMenu("Track");
|
||||
fSubTitleTrackMenu = new BMenu("Subtitles");
|
||||
fAttributesMenu = new BMenu("Attributes");
|
||||
|
||||
fMenuBar->AddItem(fFileMenu);
|
||||
@@ -1446,6 +1471,7 @@ MainWin::_CreateMenu()
|
||||
fAudioMenu->AddItem(fAudioTrackMenu);
|
||||
|
||||
fVideoMenu->AddItem(fVideoTrackMenu);
|
||||
fVideoMenu->AddItem(fSubTitleTrackMenu);
|
||||
fVideoMenu->AddSeparatorItem();
|
||||
BMessage* resizeMessage = new BMessage(M_VIEW_SIZE);
|
||||
resizeMessage->AddInt32("size", 50);
|
||||
@@ -1544,10 +1570,12 @@ MainWin::_SetupVideoAspectItems(BMenu* menu)
|
||||
|
||||
|
||||
void
|
||||
MainWin::_SetupTrackMenus(BMenu* audioTrackMenu, BMenu* videoTrackMenu)
|
||||
MainWin::_SetupTrackMenus(BMenu* audioTrackMenu, BMenu* videoTrackMenu,
|
||||
BMenu* subTitleTrackMenu)
|
||||
{
|
||||
audioTrackMenu->RemoveItems(0, audioTrackMenu->CountItems(), true);
|
||||
videoTrackMenu->RemoveItems(0, videoTrackMenu->CountItems(), true);
|
||||
subTitleTrackMenu->RemoveItems(0, subTitleTrackMenu->CountItems(), true);
|
||||
|
||||
char s[100];
|
||||
|
||||
@@ -1590,6 +1618,33 @@ MainWin::_SetupTrackMenus(BMenu* audioTrackMenu, BMenu* videoTrackMenu)
|
||||
videoTrackMenu->AddItem(new BMenuItem("none", new BMessage(M_DUMMY)));
|
||||
videoTrackMenu->ItemAt(0)->SetMarked(true);
|
||||
}
|
||||
|
||||
count = fController->SubTitleTrackCount();
|
||||
if (count > 0) {
|
||||
current = fController->CurrentSubTitleTrack();
|
||||
BMenuItem* item = new BMenuItem("Off",
|
||||
new BMessage(M_SELECT_SUB_TITLE_TRACK - 1));
|
||||
subTitleTrackMenu->AddItem(item);
|
||||
item->SetMarked(current == -1);
|
||||
|
||||
subTitleTrackMenu->AddSeparatorItem();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char* name = fController->SubTitleTrackName(i);
|
||||
if (name != NULL)
|
||||
snprintf(s, sizeof(s), "%s", name);
|
||||
else
|
||||
snprintf(s, sizeof(s), "Track %d", i + 1);
|
||||
item = new BMenuItem(s,
|
||||
new BMessage(M_SELECT_SUB_TITLE_TRACK + i));
|
||||
item->SetMarked(i == current);
|
||||
subTitleTrackMenu->AddItem(item);
|
||||
}
|
||||
} else {
|
||||
subTitleTrackMenu->AddItem(new BMenuItem("none",
|
||||
new BMessage(M_DUMMY)));
|
||||
subTitleTrackMenu->ItemAt(0)->SetMarked(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1945,10 +2000,12 @@ MainWin::_ShowContextMenu(const BPoint& screenPoint)
|
||||
// Add track selector menus
|
||||
BMenu* audioTrackMenu = new BMenu("Audio track");
|
||||
BMenu* videoTrackMenu = new BMenu("Video track");
|
||||
_SetupTrackMenus(audioTrackMenu, videoTrackMenu);
|
||||
BMenu* subTitleTrackMenu = new BMenu("Subtitles");
|
||||
_SetupTrackMenus(audioTrackMenu, videoTrackMenu, subTitleTrackMenu);
|
||||
|
||||
audioTrackMenu->SetTargetForItems(this);
|
||||
videoTrackMenu->SetTargetForItems(this);
|
||||
subTitleTrackMenu->SetTargetForItems(this);
|
||||
|
||||
menu->AddItem(item = new BMenuItem(audioTrackMenu));
|
||||
item->SetEnabled(fHasAudio);
|
||||
@@ -1956,6 +2013,9 @@ MainWin::_ShowContextMenu(const BPoint& screenPoint)
|
||||
menu->AddItem(item = new BMenuItem(videoTrackMenu));
|
||||
item->SetEnabled(fHasVideo);
|
||||
|
||||
menu->AddItem(item = new BMenuItem(subTitleTrackMenu));
|
||||
item->SetEnabled(fHasVideo);
|
||||
|
||||
menu->AddSeparatorItem();
|
||||
menu->AddItem(new BMenuItem("Quit", new BMessage(M_FILE_QUIT), 'Q'));
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ private:
|
||||
void _CreateMenu();
|
||||
void _SetupVideoAspectItems(BMenu* menu);
|
||||
void _SetupTrackMenus(BMenu* audioTrackMenu,
|
||||
BMenu* videoTrackMenu);
|
||||
BMenu* videoTrackMenu,
|
||||
BMenu* subTitleTrackMenu);
|
||||
void _UpdateAudioChannelCount(int32 audioTrackIndex);
|
||||
|
||||
void _GetMinimumWindowSize(int& width,
|
||||
@@ -152,6 +153,7 @@ private:
|
||||
BMenu* fVideoAspectMenu;
|
||||
BMenu* fAudioTrackMenu;
|
||||
BMenu* fVideoTrackMenu;
|
||||
BMenu* fSubTitleTrackMenu;
|
||||
BMenuItem* fNoInterfaceMenuItem;
|
||||
BMenu* fAttributesMenu;
|
||||
BMenu* fRatingMenu;
|
||||
|
||||
@@ -290,15 +290,20 @@ VideoView::SetVideoFrame(const BRect& frame)
|
||||
|
||||
|
||||
void
|
||||
VideoView::SetSubtitle(const char* text)
|
||||
VideoView::SetSubTitle(const char* text)
|
||||
{
|
||||
if (text == NULL || text[0] == '\0') {
|
||||
fHasSubtitle = false;
|
||||
return;
|
||||
} else {
|
||||
fHasSubtitle = true;
|
||||
fSubtitleBitmap->SetText(text);
|
||||
}
|
||||
// TODO: Make smarter and invalidate only previous subtitle bitmap
|
||||
// region;
|
||||
if (!fIsPlaying && LockLooper()) {
|
||||
Invalidate();
|
||||
UnlockLooper();
|
||||
}
|
||||
|
||||
fHasSubtitle = true;
|
||||
fSubtitleBitmap->SetText(text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
void SetFullscreen(bool fullScreen);
|
||||
void SetVideoFrame(const BRect& frame);
|
||||
|
||||
void SetSubtitle(const char* text);
|
||||
void SetSubTitle(const char* text);
|
||||
|
||||
private:
|
||||
void _DrawBitmap(const BBitmap* bitmap);
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <Bitmap.h>
|
||||
#include <TextView.h>
|
||||
|
||||
#include "StackBlurFilter.h"
|
||||
|
||||
|
||||
SubtitleBitmap::SubtitleBitmap()
|
||||
:
|
||||
@@ -77,11 +79,15 @@ SubtitleBitmap::_GenerateBitmap()
|
||||
|
||||
delete fBitmap;
|
||||
|
||||
BRect bounds = _InsertText();
|
||||
BRect bounds;
|
||||
float outlineRadius;
|
||||
_InsertText(bounds, outlineRadius);
|
||||
|
||||
fBitmap = new BBitmap(bounds, B_BITMAP_ACCEPTS_VIEWS, B_RGBA32);
|
||||
memset(fBitmap->Bits(), 0, fBitmap->BitsLength());
|
||||
|
||||
StackBlurFilter filter;
|
||||
|
||||
if (fBitmap->Lock()) {
|
||||
fBitmap->AddChild(fShadowTextView);
|
||||
fShadowTextView->ResizeTo(bounds.Width(), bounds.Height());
|
||||
@@ -97,8 +103,11 @@ SubtitleBitmap::_GenerateBitmap()
|
||||
fShadowTextView->Sync();
|
||||
fShadowTextView->RemoveSelf();
|
||||
|
||||
filter.Filter(fBitmap, outlineRadius * 2);
|
||||
|
||||
fBitmap->AddChild(fTextView);
|
||||
fTextView->ResizeTo(bounds.Width(), bounds.Height());
|
||||
fTextView->MoveTo(-outlineRadius / 2, -outlineRadius / 2);
|
||||
|
||||
fTextView->SetViewColor(0, 0, 0, 0);
|
||||
fTextView->SetDrawingMode(B_OP_ALPHA);
|
||||
@@ -273,12 +282,12 @@ parse_text(const BString& string, BTextView* textView, const BFont& font,
|
||||
}
|
||||
|
||||
|
||||
BRect
|
||||
SubtitleBitmap::_InsertText()
|
||||
void
|
||||
SubtitleBitmap::_InsertText(BRect& textRect, float& outlineRadius)
|
||||
{
|
||||
BFont font(be_plain_font);
|
||||
float fontSize = ceilf((fVideoBounds.Width() * 0.9) / 35);
|
||||
float falseBoldWidth = ceilf(fontSize / 28.0);
|
||||
float fontSize = ceilf((fVideoBounds.Width() * 0.9) / 36);
|
||||
outlineRadius = ceilf(fontSize / 28.0);
|
||||
font.SetSize(fontSize);
|
||||
|
||||
rgb_color shadow;
|
||||
@@ -293,8 +302,8 @@ SubtitleBitmap::_InsertText()
|
||||
color.blue = 255;
|
||||
color.alpha = 240;
|
||||
|
||||
BRect textRect = fVideoBounds;
|
||||
textRect.OffsetBy(falseBoldWidth, falseBoldWidth);
|
||||
textRect = fVideoBounds;
|
||||
textRect.OffsetBy(outlineRadius, outlineRadius);
|
||||
|
||||
fTextView->SetText(NULL);
|
||||
fTextView->SetFontAndColor(&font, B_FONT_ALL, &color);
|
||||
@@ -302,7 +311,7 @@ SubtitleBitmap::_InsertText()
|
||||
fTextView->Insert(" ");
|
||||
parse_text(fText, fTextView, font, color, true);
|
||||
|
||||
font.SetFalseBoldWidth(falseBoldWidth);
|
||||
font.SetFalseBoldWidth(outlineRadius);
|
||||
fShadowTextView->SetText(NULL);
|
||||
fShadowTextView->SetFontAndColor(&font, B_FONT_ALL, &shadow);
|
||||
|
||||
@@ -316,9 +325,16 @@ SubtitleBitmap::_InsertText()
|
||||
fShadowTextView->SetTextRect(textRect);
|
||||
|
||||
textRect = fTextView->TextRect();
|
||||
textRect.InsetBy(-falseBoldWidth, -falseBoldWidth);
|
||||
textRect.InsetBy(-outlineRadius, -outlineRadius);
|
||||
textRect.OffsetTo(B_ORIGIN);
|
||||
return textRect;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ public:
|
||||
|
||||
private:
|
||||
void _GenerateBitmap();
|
||||
BRect _InsertText();
|
||||
void _InsertText(BRect& bounds,
|
||||
float& outlineRadius);
|
||||
|
||||
private:
|
||||
BBitmap* fBitmap;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <Path.h>
|
||||
|
||||
#include "MediaFileTrackSupplier.h"
|
||||
#include "SubTitlesSRT.h"
|
||||
|
||||
|
||||
static const char* kPathKey = "path";
|
||||
@@ -346,10 +347,64 @@ FilePlaylistItem::CreateTrackSupplier() const
|
||||
BMediaFile* mediaFile = new(std::nothrow) BMediaFile(&fRef);
|
||||
if (mediaFile == NULL)
|
||||
return NULL;
|
||||
TrackSupplier* supplier
|
||||
MediaFileTrackSupplier* supplier
|
||||
= new(std::nothrow) MediaFileTrackSupplier(mediaFile);
|
||||
if (supplier == NULL)
|
||||
if (supplier == NULL) {
|
||||
delete mediaFile;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Search for subtitle files in the same folder
|
||||
// TODO: Error checking
|
||||
BEntry entry(&fRef, true);
|
||||
|
||||
char originalName[B_FILE_NAME_LENGTH];
|
||||
entry.GetName(originalName);
|
||||
BString nameWithoutExtension(originalName);
|
||||
int32 extension = nameWithoutExtension.FindLast('.');
|
||||
if (extension > 0)
|
||||
nameWithoutExtension.Truncate(extension);
|
||||
|
||||
BPath path;
|
||||
entry.GetPath(&path);
|
||||
path.GetParent(&path);
|
||||
BDirectory directory(path.Path());
|
||||
while (directory.GetNextEntry(&entry) == B_OK) {
|
||||
char name[B_FILE_NAME_LENGTH];
|
||||
if (entry.GetName(name) != B_OK)
|
||||
continue;
|
||||
BString nameString(name);
|
||||
if (nameString == originalName)
|
||||
continue;
|
||||
if (nameString.IFindFirst(nameWithoutExtension) < 0)
|
||||
continue;
|
||||
|
||||
BFile file(&entry, B_READ_ONLY);
|
||||
if (file.InitCheck() != B_OK)
|
||||
continue;
|
||||
|
||||
int32 pos = nameString.FindLast('.');
|
||||
if (pos < 0)
|
||||
continue;
|
||||
|
||||
BString extensionString(nameString.String() + pos + 1);
|
||||
extensionString.ToLower();
|
||||
|
||||
BString language = "default";
|
||||
if (pos > 1) {
|
||||
int32 end = pos;
|
||||
while (pos > 0 && *(nameString.String() + pos - 1) != '.')
|
||||
pos--;
|
||||
language.SetTo(nameString.String() + pos, end - pos);
|
||||
}
|
||||
|
||||
if (extensionString == "srt") {
|
||||
SubTitles* subTitles
|
||||
= new(std::nothrow) SubTitlesSRT(&file, language.String());
|
||||
if (subTitles != NULL && !supplier->AddSubTitles(subTitles))
|
||||
delete subTitles;
|
||||
}
|
||||
}
|
||||
|
||||
return supplier;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,9 @@ MediaFileTrackSupplier::~MediaFileTrackSupplier()
|
||||
{
|
||||
delete fMediaFile;
|
||||
// BMediaFile destructor will call ReleaseAllTracks()
|
||||
}
|
||||
for (int32 i = fSubTitleTracks.CountItems() - 1; i >= 0; i--)
|
||||
delete reinterpret_cast<SubTitles*>(fSubTitleTracks.ItemAtFast(i));
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
@@ -115,6 +117,13 @@ MediaFileTrackSupplier::CountVideoTracks()
|
||||
}
|
||||
|
||||
|
||||
int32
|
||||
MediaFileTrackSupplier::CountSubTitleTracks()
|
||||
{
|
||||
return fSubTitleTracks.CountItems();
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
MediaFileTrackSupplier::GetAudioMetaData(int32 index, BMessage* metaData)
|
||||
{
|
||||
@@ -166,3 +175,17 @@ MediaFileTrackSupplier::CreateVideoTrackForIndex(int32 index)
|
||||
return supplier;
|
||||
}
|
||||
|
||||
|
||||
const SubTitles*
|
||||
MediaFileTrackSupplier::SubTitleTrackForIndex(int32 index)
|
||||
{
|
||||
return reinterpret_cast<SubTitles*>(fSubTitleTracks.ItemAt(index));
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
MediaFileTrackSupplier::AddSubTitles(SubTitles* subTitles)
|
||||
{
|
||||
return fSubTitleTracks.AddItem(subTitles);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
|
||||
virtual int32 CountAudioTracks();
|
||||
virtual int32 CountVideoTracks();
|
||||
virtual int32 CountSubTitleTracks();
|
||||
|
||||
virtual status_t GetAudioMetaData(int32 index,
|
||||
BMessage* metaData);
|
||||
@@ -36,11 +37,15 @@ public:
|
||||
|
||||
virtual AudioTrackSupplier* CreateAudioTrackForIndex(int32 index);
|
||||
virtual VideoTrackSupplier* CreateVideoTrackForIndex(int32 index);
|
||||
virtual const SubTitles* SubTitleTrackForIndex(int32 index);
|
||||
|
||||
bool AddSubTitles(SubTitles* subTitles);
|
||||
|
||||
private:
|
||||
BMediaFile* fMediaFile;
|
||||
BList fAudioTracks;
|
||||
BList fVideoTracks;
|
||||
BList fSubTitleTracks;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2010, Stephan Aßmus <superstippi@gmx.de>. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*/
|
||||
|
||||
|
||||
#include "SubTitles.h"
|
||||
|
||||
|
||||
SubTitles::SubTitles()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
SubTitles::~SubTitles()
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2010, Stephan Aßmus <superstippi@gmx.de>. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*/
|
||||
#ifndef SUB_TITLES_H
|
||||
#define SUB_TITLES_H
|
||||
|
||||
|
||||
#include <Point.h>
|
||||
#include <String.h>
|
||||
|
||||
|
||||
class BFile;
|
||||
|
||||
|
||||
struct SubTitle {
|
||||
BString text;
|
||||
BPoint placement;
|
||||
bigtime_t startTime;
|
||||
bigtime_t duration;
|
||||
};
|
||||
|
||||
|
||||
class SubTitles {
|
||||
public:
|
||||
SubTitles();
|
||||
virtual ~SubTitles();
|
||||
|
||||
virtual const char* Name() const = 0;
|
||||
virtual const SubTitle* SubTitleAt(bigtime_t time) const = 0;
|
||||
};
|
||||
|
||||
|
||||
#endif //SUB_TITLES_H
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2010, Stephan Aßmus <superstippi@gmx.de>. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*/
|
||||
|
||||
|
||||
#include "SubTitlesSRT.h"
|
||||
|
||||
#include <new>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <File.h>
|
||||
|
||||
#include "FileReadWrite.h"
|
||||
|
||||
|
||||
SubTitlesSRT::SubTitlesSRT(BFile* file, const char* name)
|
||||
:
|
||||
SubTitles(),
|
||||
fName(name),
|
||||
fSubTitles(64)
|
||||
{
|
||||
if (file == NULL)
|
||||
return;
|
||||
if (file->InitCheck() != B_OK)
|
||||
return;
|
||||
|
||||
FileReadWrite lineProvider(file);
|
||||
BString line;
|
||||
enum {
|
||||
EXPECT_SEQUENCE_NUMBER = 0,
|
||||
EXPECT_TIME_CODE,
|
||||
EXPECT_TEXT
|
||||
};
|
||||
|
||||
SubTitle subTitle;
|
||||
int32 lastSequenceNumber = 0;
|
||||
int32 currentLine = 0;
|
||||
|
||||
int32 state = EXPECT_SEQUENCE_NUMBER;
|
||||
while (lineProvider.Next(line)) {
|
||||
line.RemoveAll("\n");
|
||||
line.RemoveAll("\r");
|
||||
switch (state) {
|
||||
case EXPECT_SEQUENCE_NUMBER:
|
||||
{
|
||||
line.Trim();
|
||||
int32 sequenceNumber = atoi(line.String());
|
||||
if (sequenceNumber != lastSequenceNumber + 1) {
|
||||
fprintf(stderr, "Warning: Wrong sequence number in SRT "
|
||||
"file: %ld, expected: %ld, line %ld\n", sequenceNumber,
|
||||
lastSequenceNumber + 1, currentLine);
|
||||
}
|
||||
state = EXPECT_TIME_CODE;
|
||||
lastSequenceNumber = sequenceNumber;
|
||||
break;
|
||||
}
|
||||
|
||||
case EXPECT_TIME_CODE:
|
||||
{
|
||||
line.Trim();
|
||||
int32 separatorPos = line.FindFirst(" --> ");
|
||||
if (separatorPos < 0) {
|
||||
fprintf(stderr, "Error: Time code expected on line %ld, "
|
||||
"got '%s'\n", currentLine, line.String());
|
||||
return;
|
||||
}
|
||||
BString timeCode(line.String(), separatorPos);
|
||||
if (separatorPos != 12) {
|
||||
fprintf(stderr, "Warning: Time code broken on line %ld "
|
||||
"(%s)?\n", currentLine, timeCode.String());
|
||||
}
|
||||
int hours;
|
||||
int minutes;
|
||||
int seconds;
|
||||
int milliSeconds;
|
||||
if (sscanf(timeCode.String(), "%d:%d:%d,%d", &hours, &minutes,
|
||||
&seconds, &milliSeconds) != 4) {
|
||||
fprintf(stderr, "Error: Failed to parse start time on "
|
||||
"line %ld\n", currentLine);
|
||||
return;
|
||||
}
|
||||
subTitle.startTime = (bigtime_t)hours * 60 * 60 * 1000000LL
|
||||
+ (bigtime_t)minutes * 60 * 1000000LL
|
||||
+ (bigtime_t)seconds * 1000000LL
|
||||
+ (bigtime_t)milliSeconds * 1000;
|
||||
|
||||
int32 endTimePos = separatorPos + 5;
|
||||
timeCode.SetTo(line.String() + endTimePos);
|
||||
if (sscanf(timeCode.String(), "%d:%d:%d,%d", &hours, &minutes,
|
||||
&seconds, &milliSeconds) != 4) {
|
||||
fprintf(stderr, "Error: Failed to parse end time on "
|
||||
"line %ld\n", currentLine);
|
||||
return;
|
||||
}
|
||||
bigtime_t endTime = (bigtime_t)hours * 60 * 60 * 1000000LL
|
||||
+ (bigtime_t)minutes * 60 * 1000000LL
|
||||
+ (bigtime_t)seconds * 1000000LL
|
||||
+ (bigtime_t)milliSeconds * 1000;
|
||||
|
||||
subTitle.duration = endTime - subTitle.startTime;
|
||||
|
||||
state = EXPECT_TEXT;
|
||||
break;
|
||||
}
|
||||
|
||||
case EXPECT_TEXT:
|
||||
if (line.Length() == 0) {
|
||||
int32 index = _IndexFor(subTitle.startTime);
|
||||
SubTitle* clone = new(std::nothrow) SubTitle(subTitle);
|
||||
if (clone == NULL || !fSubTitles.AddItem(clone, index)) {
|
||||
delete clone;
|
||||
return;
|
||||
}
|
||||
subTitle.text = "";
|
||||
subTitle.placement = BPoint(-1, -1);
|
||||
subTitle.startTime = 0;
|
||||
subTitle.duration = 0;
|
||||
|
||||
state = EXPECT_SEQUENCE_NUMBER;
|
||||
} else
|
||||
subTitle.text << line << '\n';
|
||||
break;
|
||||
}
|
||||
line.SetTo("");
|
||||
currentLine++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SubTitlesSRT::~SubTitlesSRT()
|
||||
{
|
||||
for (int32 i = fSubTitles.CountItems() - 1; i >= 0; i--)
|
||||
delete reinterpret_cast<SubTitle*>(fSubTitles.ItemAtFast(i));
|
||||
}
|
||||
|
||||
|
||||
const char*
|
||||
SubTitlesSRT::Name() const
|
||||
{
|
||||
return fName.String();
|
||||
}
|
||||
|
||||
|
||||
const SubTitle*
|
||||
SubTitlesSRT::SubTitleAt(bigtime_t time) const
|
||||
{
|
||||
int32 index = _IndexFor(time);
|
||||
SubTitle* subTitle
|
||||
= reinterpret_cast<SubTitle*>(fSubTitles.ItemAt(index));
|
||||
if (subTitle != NULL && subTitle->startTime > time)
|
||||
subTitle = reinterpret_cast<SubTitle*>(fSubTitles.ItemAt(index - 1));
|
||||
if (subTitle != NULL && subTitle->startTime <= time
|
||||
&& subTitle->startTime + subTitle->duration > time) {
|
||||
return subTitle;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
int32
|
||||
SubTitlesSRT::_IndexFor(bigtime_t startTime) const
|
||||
{
|
||||
// binary search index
|
||||
int32 lower = 0;
|
||||
int32 upper = fSubTitles.CountItems();
|
||||
while (lower < upper) {
|
||||
int32 mid = (lower + upper) / 2;
|
||||
SubTitle* subTitle = reinterpret_cast<SubTitle*>(
|
||||
fSubTitles.ItemAtFast(mid));
|
||||
if (startTime < subTitle->startTime)
|
||||
upper = mid;
|
||||
else
|
||||
lower = mid + 1;
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2010, Stephan Aßmus <superstippi@gmx.de>. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*/
|
||||
#ifndef SUB_TITLES_SRT_H
|
||||
#define SUB_TITLES_SRT_H
|
||||
|
||||
|
||||
#include <List.h>
|
||||
|
||||
#include "SubTitles.h"
|
||||
|
||||
|
||||
class BFile;
|
||||
|
||||
|
||||
class SubTitlesSRT : public SubTitles {
|
||||
public:
|
||||
SubTitlesSRT(BFile* file, const char* name);
|
||||
virtual ~SubTitlesSRT();
|
||||
|
||||
virtual const char* Name() const;
|
||||
virtual const SubTitle* SubTitleAt(bigtime_t time) const;
|
||||
|
||||
private:
|
||||
int32 _IndexFor(bigtime_t startTime) const;
|
||||
|
||||
BString fName;
|
||||
BList fSubTitles;
|
||||
};
|
||||
|
||||
|
||||
#endif //SUB_TITLES_SRT_H
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <MediaFormats.h>
|
||||
|
||||
#include "AudioTrackSupplier.h"
|
||||
#include "SubTitles.h"
|
||||
#include "VideoTrackSupplier.h"
|
||||
|
||||
|
||||
@@ -31,6 +32,7 @@ public:
|
||||
|
||||
virtual int32 CountAudioTracks() = 0;
|
||||
virtual int32 CountVideoTracks() = 0;
|
||||
virtual int32 CountSubTitleTracks() = 0;
|
||||
|
||||
virtual status_t GetAudioMetaData(int32 index,
|
||||
BMessage* metaData) = 0;
|
||||
@@ -39,6 +41,7 @@ public:
|
||||
|
||||
virtual AudioTrackSupplier* CreateAudioTrackForIndex(int32 index) = 0;
|
||||
virtual VideoTrackSupplier* CreateVideoTrackForIndex(int32 index) = 0;
|
||||
virtual const SubTitles* SubTitleTrackForIndex(int32 index) = 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -54,20 +54,22 @@ FileReadWrite::Next(BString& string)
|
||||
while (fAmtRead > 0) {
|
||||
while (fPositionInBuffer < fAmtRead) {
|
||||
// Return true if we hit a newline or the end of the file
|
||||
// TODO: If the source file is expected to have different encoding,
|
||||
// don't we need to check for the line endings in that encoding?!
|
||||
if (fBuffer[fPositionInBuffer] == '\n') {
|
||||
fPositionInBuffer++;
|
||||
// Convert begin
|
||||
int32 state = 0;
|
||||
int32 bufferLen = string.Length();
|
||||
int32 destBufferLen = bufferLen;
|
||||
char destination[destBufferLen];
|
||||
if (fSourceEncoding) {
|
||||
if (fSourceEncoding != -1) {
|
||||
int32 state = 0;
|
||||
int32 bufferLen = string.Length();
|
||||
int32 destBufferLen = bufferLen;
|
||||
char destination[destBufferLen];
|
||||
convert_to_utf8(fSourceEncoding, string.String(),
|
||||
&bufferLen, destination, &destBufferLen, &state);
|
||||
string = destination;
|
||||
}
|
||||
string = destination;
|
||||
return true;
|
||||
}
|
||||
// TODO: Adding one char at a time is very inefficient!
|
||||
string += fBuffer[fPositionInBuffer];
|
||||
fPositionInBuffer++;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
* Copyright 2008-2010 Stephan Aßmus <superstippi@gmx.de>
|
||||
* Distributed under the terms of the MIT license.
|
||||
*/
|
||||
//----------------------------------------------------------------------------
|
||||
// Anti-Grain Geometry - Version 2.4
|
||||
// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com)
|
||||
//
|
||||
// Permission to copy, use, modify, sell and distribute this software
|
||||
// is granted provided this copyright notice appears in all copies.
|
||||
// This software is provided "as is" without express or implied
|
||||
// warranty, and with no claim as to its suitability for any purpose.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
// Contact: [email protected]
|
||||
// [email protected]
|
||||
// http://www.antigrain.com
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// The Stack Blur Algorithm was invented by Mario Klingemann,
|
||||
// [email protected] and described here:
|
||||
// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php
|
||||
// (search phrase "Stackblur: Fast But Goodlooking").
|
||||
// The major improvement is that there's no more division table
|
||||
// that was very expensive to create for large blur radii. Insted,
|
||||
// for 8-bit per channel and radius not exceeding 254 the division is
|
||||
// replaced by multiplication and shift.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
|
||||
#include "StackBlurFilter.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <Bitmap.h>
|
||||
|
||||
#include <agg_array.h>
|
||||
|
||||
|
||||
template<class T> struct stack_blur_tables
|
||||
{
|
||||
static uint16 const g_stack_blur8_mul[255];
|
||||
static uint8 const g_stack_blur8_shr[255];
|
||||
};
|
||||
|
||||
|
||||
template<class T>
|
||||
uint16 const stack_blur_tables<T>::g_stack_blur8_mul[255] =
|
||||
{
|
||||
512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,
|
||||
454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,
|
||||
482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,
|
||||
437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,
|
||||
497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,
|
||||
320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,
|
||||
446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,
|
||||
329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,
|
||||
505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,
|
||||
399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,
|
||||
324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,
|
||||
268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,
|
||||
451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,
|
||||
385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,
|
||||
332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,
|
||||
289,287,285,282,280,278,275,273,271,269,267,265,263,261,259
|
||||
};
|
||||
|
||||
|
||||
template<class T>
|
||||
uint8 const stack_blur_tables<T>::g_stack_blur8_shr[255] =
|
||||
{
|
||||
9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
|
||||
17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
|
||||
19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
|
||||
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
|
||||
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
|
||||
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
|
||||
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
|
||||
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
|
||||
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
|
||||
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
|
||||
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
|
||||
23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24
|
||||
};
|
||||
|
||||
|
||||
#if __GNUC__ < 4
|
||||
static inline float
|
||||
roundf(float v)
|
||||
{
|
||||
return floorf(v + 0.5);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
StackBlurFilter::StackBlurFilter()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
StackBlurFilter::~StackBlurFilter()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
StackBlurFilter::Filter(BBitmap* bitmap, double radius)
|
||||
{
|
||||
if (radius < 1.0)
|
||||
return;
|
||||
|
||||
int32 width = bitmap->Bounds().IntegerWidth() + 1;
|
||||
int32 height = bitmap->Bounds().IntegerHeight() + 1;
|
||||
uint32 bpr = bitmap->BytesPerRow();
|
||||
uint8* bits = (uint8*)bitmap->Bits();
|
||||
|
||||
unsigned r = (unsigned)roundf(radius);
|
||||
|
||||
if (bitmap->ColorSpace() == B_RGBA32
|
||||
|| bitmap->ColorSpace() == B_RGB32) {
|
||||
|
||||
_Filter32(bits, width, height, bpr, r, r);
|
||||
|
||||
} else if (bitmap->ColorSpace() == B_GRAY8) {
|
||||
|
||||
_Filter8(bits, width, height, bpr, r, r);
|
||||
|
||||
} else
|
||||
printf("StackBlurFilter::Filter() - unsupported color space\n");
|
||||
}
|
||||
|
||||
|
||||
// #pragma mark -
|
||||
|
||||
|
||||
struct rgba {
|
||||
uint8 r;
|
||||
uint8 g;
|
||||
uint8 b;
|
||||
uint8 a;
|
||||
};
|
||||
|
||||
|
||||
void
|
||||
StackBlurFilter::_Filter32(uint8* buffer, unsigned width, unsigned height,
|
||||
int32 bpr, unsigned rx, unsigned ry) const
|
||||
{
|
||||
typedef rgba color_type;
|
||||
|
||||
unsigned x, y, xp, yp, i;
|
||||
unsigned stack_ptr;
|
||||
unsigned stack_start;
|
||||
|
||||
const uint8* src_pix_ptr;
|
||||
uint8* dst_pix_ptr;
|
||||
color_type* stack_pix_ptr;
|
||||
|
||||
unsigned sum_r;
|
||||
unsigned sum_g;
|
||||
unsigned sum_b;
|
||||
unsigned sum_a;
|
||||
unsigned sum_in_r;
|
||||
unsigned sum_in_g;
|
||||
unsigned sum_in_b;
|
||||
unsigned sum_in_a;
|
||||
unsigned sum_out_r;
|
||||
unsigned sum_out_g;
|
||||
unsigned sum_out_b;
|
||||
unsigned sum_out_a;
|
||||
|
||||
unsigned w = width;
|
||||
unsigned h = height;
|
||||
unsigned wm = w - 1;
|
||||
unsigned hm = h - 1;
|
||||
|
||||
unsigned div;
|
||||
unsigned mul_sum;
|
||||
unsigned shr_sum;
|
||||
|
||||
agg::pod_vector<color_type> stack;
|
||||
|
||||
if (rx > 0) {
|
||||
if (rx > 254)
|
||||
rx = 254;
|
||||
div = rx * 2 + 1;
|
||||
mul_sum = stack_blur_tables<int>::g_stack_blur8_mul[rx];
|
||||
shr_sum = stack_blur_tables<int>::g_stack_blur8_shr[rx];
|
||||
stack.allocate(div);
|
||||
|
||||
for (y = 0; y < h; y++) {
|
||||
sum_r =
|
||||
sum_g =
|
||||
sum_b =
|
||||
sum_a =
|
||||
sum_in_r =
|
||||
sum_in_g =
|
||||
sum_in_b =
|
||||
sum_in_a =
|
||||
sum_out_r =
|
||||
sum_out_g =
|
||||
sum_out_b =
|
||||
sum_out_a = 0;
|
||||
|
||||
src_pix_ptr = buffer + bpr * y;
|
||||
for (i = 0; i <= rx; i++) {
|
||||
stack_pix_ptr = &stack[i];
|
||||
stack_pix_ptr->r = src_pix_ptr[2];
|
||||
stack_pix_ptr->g = src_pix_ptr[1];
|
||||
stack_pix_ptr->b = src_pix_ptr[0];
|
||||
stack_pix_ptr->a = src_pix_ptr[3];
|
||||
sum_r += src_pix_ptr[2] * (i + 1);
|
||||
sum_g += src_pix_ptr[1] * (i + 1);
|
||||
sum_b += src_pix_ptr[0] * (i + 1);
|
||||
sum_a += src_pix_ptr[3] * (i + 1);
|
||||
sum_out_r += src_pix_ptr[2];
|
||||
sum_out_g += src_pix_ptr[1];
|
||||
sum_out_b += src_pix_ptr[0];
|
||||
sum_out_a += src_pix_ptr[3];
|
||||
}
|
||||
for (i = 1; i <= rx; i++) {
|
||||
if (i <= wm)
|
||||
src_pix_ptr += 4;
|
||||
stack_pix_ptr = &stack[i + rx];
|
||||
stack_pix_ptr->r = src_pix_ptr[2];
|
||||
stack_pix_ptr->g = src_pix_ptr[1];
|
||||
stack_pix_ptr->b = src_pix_ptr[0];
|
||||
stack_pix_ptr->a = src_pix_ptr[3];
|
||||
sum_r += src_pix_ptr[2] * (rx + 1 - i);
|
||||
sum_g += src_pix_ptr[1] * (rx + 1 - i);
|
||||
sum_b += src_pix_ptr[0] * (rx + 1 - i);
|
||||
sum_a += src_pix_ptr[3] * (rx + 1 - i);
|
||||
sum_in_r += src_pix_ptr[2];
|
||||
sum_in_g += src_pix_ptr[1];
|
||||
sum_in_b += src_pix_ptr[0];
|
||||
sum_in_a += src_pix_ptr[3];
|
||||
}
|
||||
|
||||
stack_ptr = rx;
|
||||
xp = rx;
|
||||
if (xp > wm)
|
||||
xp = wm;
|
||||
src_pix_ptr = buffer + xp * 4 + y * bpr;
|
||||
dst_pix_ptr = buffer + y * bpr;
|
||||
for (x = 0; x < w; x++) {
|
||||
dst_pix_ptr[2] = (sum_r * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr[1] = (sum_g * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr[0] = (sum_b * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr[3] = (sum_a * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr += 4;
|
||||
|
||||
sum_r -= sum_out_r;
|
||||
sum_g -= sum_out_g;
|
||||
sum_b -= sum_out_b;
|
||||
sum_a -= sum_out_a;
|
||||
|
||||
stack_start = stack_ptr + div - rx;
|
||||
if(stack_start >= div) stack_start -= div;
|
||||
stack_pix_ptr = &stack[stack_start];
|
||||
|
||||
sum_out_r -= stack_pix_ptr->r;
|
||||
sum_out_g -= stack_pix_ptr->g;
|
||||
sum_out_b -= stack_pix_ptr->b;
|
||||
sum_out_a -= stack_pix_ptr->a;
|
||||
|
||||
if (xp < wm) {
|
||||
src_pix_ptr += 4;
|
||||
++xp;
|
||||
}
|
||||
|
||||
stack_pix_ptr->r = src_pix_ptr[2];
|
||||
stack_pix_ptr->g = src_pix_ptr[1];
|
||||
stack_pix_ptr->b = src_pix_ptr[0];
|
||||
stack_pix_ptr->a = src_pix_ptr[3];
|
||||
|
||||
sum_in_r += src_pix_ptr[2];
|
||||
sum_in_g += src_pix_ptr[1];
|
||||
sum_in_b += src_pix_ptr[0];
|
||||
sum_in_a += src_pix_ptr[3];
|
||||
sum_r += sum_in_r;
|
||||
sum_g += sum_in_g;
|
||||
sum_b += sum_in_b;
|
||||
sum_a += sum_in_a;
|
||||
|
||||
++stack_ptr;
|
||||
if (stack_ptr >= div)
|
||||
stack_ptr = 0;
|
||||
stack_pix_ptr = &stack[stack_ptr];
|
||||
|
||||
sum_out_r += stack_pix_ptr->r;
|
||||
sum_out_g += stack_pix_ptr->g;
|
||||
sum_out_b += stack_pix_ptr->b;
|
||||
sum_out_a += stack_pix_ptr->a;
|
||||
sum_in_r -= stack_pix_ptr->r;
|
||||
sum_in_g -= stack_pix_ptr->g;
|
||||
sum_in_b -= stack_pix_ptr->b;
|
||||
sum_in_a -= stack_pix_ptr->a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ry > 0) {
|
||||
if (ry > 254)
|
||||
ry = 254;
|
||||
div = ry * 2 + 1;
|
||||
mul_sum = stack_blur_tables<int>::g_stack_blur8_mul[ry];
|
||||
shr_sum = stack_blur_tables<int>::g_stack_blur8_shr[ry];
|
||||
stack.allocate(div);
|
||||
|
||||
int stride = bpr;
|
||||
for(x = 0; x < w; x++) {
|
||||
sum_r =
|
||||
sum_g =
|
||||
sum_b =
|
||||
sum_a =
|
||||
sum_in_r =
|
||||
sum_in_g =
|
||||
sum_in_b =
|
||||
sum_in_a =
|
||||
sum_out_r =
|
||||
sum_out_g =
|
||||
sum_out_b =
|
||||
sum_out_a = 0;
|
||||
|
||||
src_pix_ptr = buffer + x * 4;
|
||||
for (i = 0; i <= ry; i++) {
|
||||
stack_pix_ptr = &stack[i];
|
||||
stack_pix_ptr->r = src_pix_ptr[2];
|
||||
stack_pix_ptr->g = src_pix_ptr[1];
|
||||
stack_pix_ptr->b = src_pix_ptr[0];
|
||||
stack_pix_ptr->a = src_pix_ptr[3];
|
||||
sum_r += src_pix_ptr[2] * (i + 1);
|
||||
sum_g += src_pix_ptr[1] * (i + 1);
|
||||
sum_b += src_pix_ptr[0] * (i + 1);
|
||||
sum_a += src_pix_ptr[3] * (i + 1);
|
||||
sum_out_r += src_pix_ptr[2];
|
||||
sum_out_g += src_pix_ptr[1];
|
||||
sum_out_b += src_pix_ptr[0];
|
||||
sum_out_a += src_pix_ptr[3];
|
||||
}
|
||||
for (i = 1; i <= ry; i++) {
|
||||
if (i <= hm)
|
||||
src_pix_ptr += stride;
|
||||
stack_pix_ptr = &stack[i + ry];
|
||||
stack_pix_ptr->r = src_pix_ptr[2];
|
||||
stack_pix_ptr->g = src_pix_ptr[1];
|
||||
stack_pix_ptr->b = src_pix_ptr[0];
|
||||
stack_pix_ptr->a = src_pix_ptr[3];
|
||||
sum_r += src_pix_ptr[2] * (ry + 1 - i);
|
||||
sum_g += src_pix_ptr[1] * (ry + 1 - i);
|
||||
sum_b += src_pix_ptr[0] * (ry + 1 - i);
|
||||
sum_a += src_pix_ptr[3] * (ry + 1 - i);
|
||||
sum_in_r += src_pix_ptr[2];
|
||||
sum_in_g += src_pix_ptr[1];
|
||||
sum_in_b += src_pix_ptr[0];
|
||||
sum_in_a += src_pix_ptr[3];
|
||||
}
|
||||
|
||||
stack_ptr = ry;
|
||||
yp = ry;
|
||||
if (yp > hm)
|
||||
yp = hm;
|
||||
src_pix_ptr = buffer + x * 4 + yp * bpr;
|
||||
dst_pix_ptr = buffer + x * 4;
|
||||
for (y = 0; y < h; y++) {
|
||||
dst_pix_ptr[2] = (sum_r * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr[1] = (sum_g * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr[0] = (sum_b * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr[3] = (sum_a * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr += stride;
|
||||
|
||||
sum_r -= sum_out_r;
|
||||
sum_g -= sum_out_g;
|
||||
sum_b -= sum_out_b;
|
||||
sum_a -= sum_out_a;
|
||||
|
||||
stack_start = stack_ptr + div - ry;
|
||||
if (stack_start >= div)
|
||||
stack_start -= div;
|
||||
|
||||
stack_pix_ptr = &stack[stack_start];
|
||||
sum_out_r -= stack_pix_ptr->r;
|
||||
sum_out_g -= stack_pix_ptr->g;
|
||||
sum_out_b -= stack_pix_ptr->b;
|
||||
sum_out_a -= stack_pix_ptr->a;
|
||||
|
||||
if (yp < hm) {
|
||||
src_pix_ptr += stride;
|
||||
++yp;
|
||||
}
|
||||
|
||||
stack_pix_ptr->r = src_pix_ptr[2];
|
||||
stack_pix_ptr->g = src_pix_ptr[1];
|
||||
stack_pix_ptr->b = src_pix_ptr[0];
|
||||
stack_pix_ptr->a = src_pix_ptr[3];
|
||||
|
||||
sum_in_r += src_pix_ptr[2];
|
||||
sum_in_g += src_pix_ptr[1];
|
||||
sum_in_b += src_pix_ptr[0];
|
||||
sum_in_a += src_pix_ptr[3];
|
||||
sum_r += sum_in_r;
|
||||
sum_g += sum_in_g;
|
||||
sum_b += sum_in_b;
|
||||
sum_a += sum_in_a;
|
||||
|
||||
++stack_ptr;
|
||||
if (stack_ptr >= div)
|
||||
stack_ptr = 0;
|
||||
stack_pix_ptr = &stack[stack_ptr];
|
||||
|
||||
sum_out_r += stack_pix_ptr->r;
|
||||
sum_out_g += stack_pix_ptr->g;
|
||||
sum_out_b += stack_pix_ptr->b;
|
||||
sum_out_a += stack_pix_ptr->a;
|
||||
sum_in_r -= stack_pix_ptr->r;
|
||||
sum_in_g -= stack_pix_ptr->g;
|
||||
sum_in_b -= stack_pix_ptr->b;
|
||||
sum_in_a -= stack_pix_ptr->a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
StackBlurFilter::_Filter8(uint8* buffer, unsigned width, unsigned height,
|
||||
int32 bpr, unsigned rx, unsigned ry) const
|
||||
{
|
||||
unsigned x, y, xp, yp, i;
|
||||
unsigned stack_ptr;
|
||||
unsigned stack_start;
|
||||
|
||||
const uint8* src_pix_ptr;
|
||||
uint8* dst_pix_ptr;
|
||||
unsigned pix;
|
||||
unsigned stack_pix;
|
||||
unsigned sum;
|
||||
unsigned sum_in;
|
||||
unsigned sum_out;
|
||||
|
||||
unsigned w = width;
|
||||
unsigned h = height;
|
||||
unsigned wm = w - 1;
|
||||
unsigned hm = h - 1;
|
||||
|
||||
unsigned div;
|
||||
unsigned mul_sum;
|
||||
unsigned shr_sum;
|
||||
|
||||
agg::pod_vector<uint8> stack;
|
||||
|
||||
if(rx > 0)
|
||||
{
|
||||
if(rx > 254) rx = 254;
|
||||
div = rx * 2 + 1;
|
||||
mul_sum = stack_blur_tables<int>::g_stack_blur8_mul[rx];
|
||||
shr_sum = stack_blur_tables<int>::g_stack_blur8_shr[rx];
|
||||
stack.allocate(div);
|
||||
|
||||
for(y = 0; y < h; y++)
|
||||
{
|
||||
sum = sum_in = sum_out = 0;
|
||||
|
||||
src_pix_ptr = buffer + y * bpr;
|
||||
pix = *src_pix_ptr;
|
||||
for(i = 0; i <= rx; i++)
|
||||
{
|
||||
stack[i] = pix;
|
||||
sum += pix * (i + 1);
|
||||
sum_out += pix;
|
||||
}
|
||||
for(i = 1; i <= rx; i++)
|
||||
{
|
||||
if(i <= wm) src_pix_ptr += 1;
|
||||
pix = *src_pix_ptr;
|
||||
stack[i + rx] = pix;
|
||||
sum += pix * (rx + 1 - i);
|
||||
sum_in += pix;
|
||||
}
|
||||
|
||||
stack_ptr = rx;
|
||||
xp = rx;
|
||||
if(xp > wm) xp = wm;
|
||||
src_pix_ptr = buffer + xp + y * bpr;
|
||||
dst_pix_ptr = buffer + y * bpr;
|
||||
for(x = 0; x < w; x++)
|
||||
{
|
||||
*dst_pix_ptr = (sum * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr += 1;
|
||||
|
||||
sum -= sum_out;
|
||||
|
||||
stack_start = stack_ptr + div - rx;
|
||||
if(stack_start >= div) stack_start -= div;
|
||||
sum_out -= stack[stack_start];
|
||||
|
||||
if(xp < wm)
|
||||
{
|
||||
src_pix_ptr += 1;
|
||||
pix = *src_pix_ptr;
|
||||
++xp;
|
||||
}
|
||||
|
||||
stack[stack_start] = pix;
|
||||
|
||||
sum_in += pix;
|
||||
sum += sum_in;
|
||||
|
||||
++stack_ptr;
|
||||
if(stack_ptr >= div) stack_ptr = 0;
|
||||
stack_pix = stack[stack_ptr];
|
||||
|
||||
sum_out += stack_pix;
|
||||
sum_in -= stack_pix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(ry > 0)
|
||||
{
|
||||
if(ry > 254) ry = 254;
|
||||
div = ry * 2 + 1;
|
||||
mul_sum = stack_blur_tables<int>::g_stack_blur8_mul[ry];
|
||||
shr_sum = stack_blur_tables<int>::g_stack_blur8_shr[ry];
|
||||
stack.allocate(div);
|
||||
|
||||
int stride = bpr;
|
||||
for(x = 0; x < w; x++)
|
||||
{
|
||||
sum = sum_in = sum_out = 0;
|
||||
|
||||
src_pix_ptr = buffer + x;
|
||||
pix = *src_pix_ptr;
|
||||
for(i = 0; i <= ry; i++)
|
||||
{
|
||||
stack[i] = pix;
|
||||
sum += pix * (i + 1);
|
||||
sum_out += pix;
|
||||
}
|
||||
for(i = 1; i <= ry; i++)
|
||||
{
|
||||
if(i <= hm) src_pix_ptr += stride;
|
||||
pix = *src_pix_ptr;
|
||||
stack[i + ry] = pix;
|
||||
sum += pix * (ry + 1 - i);
|
||||
sum_in += pix;
|
||||
}
|
||||
|
||||
stack_ptr = ry;
|
||||
yp = ry;
|
||||
if(yp > hm) yp = hm;
|
||||
src_pix_ptr = buffer + x + yp * bpr;
|
||||
dst_pix_ptr = buffer + x;
|
||||
for(y = 0; y < h; y++)
|
||||
{
|
||||
*dst_pix_ptr = (sum * mul_sum) >> shr_sum;
|
||||
dst_pix_ptr += stride;
|
||||
|
||||
sum -= sum_out;
|
||||
|
||||
stack_start = stack_ptr + div - ry;
|
||||
if(stack_start >= div) stack_start -= div;
|
||||
sum_out -= stack[stack_start];
|
||||
|
||||
if(yp < hm)
|
||||
{
|
||||
src_pix_ptr += stride;
|
||||
pix = *src_pix_ptr;
|
||||
++yp;
|
||||
}
|
||||
|
||||
stack[stack_start] = pix;
|
||||
|
||||
sum_in += pix;
|
||||
sum += sum_in;
|
||||
|
||||
++stack_ptr;
|
||||
if(stack_ptr >= div) stack_ptr = 0;
|
||||
stack_pix = stack[stack_ptr];
|
||||
|
||||
sum_out += stack_pix;
|
||||
sum_in -= stack_pix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2008-2010 Stephan Aßmus <superstippi@gmx.de>
|
||||
* Distributed under the terms of the MIT license.
|
||||
*/
|
||||
//----------------------------------------------------------------------------
|
||||
// Anti-Grain Geometry - Version 2.4
|
||||
// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com)
|
||||
//
|
||||
// Permission to copy, use, modify, sell and distribute this software
|
||||
// is granted provided this copyright notice appears in all copies.
|
||||
// This software is provided "as is" without express or implied
|
||||
// warranty, and with no claim as to its suitability for any purpose.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
// Contact: [email protected]
|
||||
// [email protected]
|
||||
// http://www.antigrain.com
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// The Stack Blur Algorithm was invented by Mario Klingemann,
|
||||
// [email protected] and described here:
|
||||
// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php
|
||||
// (search phrase "Stackblur: Fast But Goodlooking").
|
||||
// The major improvement is that there's no more division table
|
||||
// that was very expensive to create for large blur radii. Insted,
|
||||
// for 8-bit per channel and radius not exceeding 254 the division is
|
||||
// replaced by multiplication and shift.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
#ifndef STACK_BLUR_FILTER
|
||||
#define STACK_BLUR_FILTER
|
||||
|
||||
#include <SupportDefs.h>
|
||||
|
||||
class BBitmap;
|
||||
class RenderBuffer;
|
||||
|
||||
class StackBlurFilter {
|
||||
public:
|
||||
StackBlurFilter();
|
||||
~StackBlurFilter();
|
||||
|
||||
|
||||
void Filter(BBitmap* bitmap, double radius);
|
||||
|
||||
private:
|
||||
void _Filter32(uint8* buffer,
|
||||
unsigned width, unsigned height,
|
||||
int32 bpr,
|
||||
unsigned rx, unsigned ry) const;
|
||||
void _Filter8(uint8* buffer,
|
||||
unsigned width, unsigned height,
|
||||
int32 bpr,
|
||||
unsigned rx, unsigned ry) const;
|
||||
};
|
||||
|
||||
#endif // STACK_BLUR_FILTER
|
||||
Reference in New Issue
Block a user