Commit 354c7ab9 authored by Derk-Jan Hartman's avatar Derk-Jan Hartman

Please test if this has any regressions

* include/codecs.h:
  - created a subtitle_data_t to be used by subtitle demuxers
    and decoders to pass information.
  - ToDo: access/dvd/es.c and spudec need to be fixed to use the palette field
    of this new struct.
* modules/codec/subsdec.c:
  - moved the decoding of ssa textlines to here.
  - ToDo: support for any tags is lacking atm., but now possible.
* modules/demux/mkv.cpp:
  - ssa is now passed undecoded to ssa subsdec.
  - ssa headers are passed to ssa subsdec via subtitle_data_t
  - ToDo: decode idx header info and fill the subtitle_data_t
  - ToDo: support for compressed vobsubs.
* modules/demux/util/sub.?:
  - moved ssa decoding out of here.
  - ToDo: add support for multiple tracks
  - ToDo: implement reading vobsub .sub files (not .idx)
parent ed6c9819
......@@ -2,7 +2,7 @@
* codecs.h: codec related structures needed by the demuxers and decoders
*****************************************************************************
* Copyright (C) 1999-2001 VideoLAN
* $Id: codecs.h,v 1.6 2003/10/19 23:12:16 hartman Exp $
* $Id: codecs.h,v 1.7 2003/11/05 00:17:50 hartman Exp $
*
* Authors: Gildas Bazin <gbazin@netcourrier.com>
*
......@@ -142,6 +142,31 @@ static inline void wf_tag_to_fourcc( uint16_t i_tag,
}
}
/**
* Structure to hold information concerning subtitles.
* Used between demuxers and decoders of subtitles.
*/
typedef struct es_sys_t
{
char *psz_header; /* for 'ssa ' and 'subt' */
/* for spudec */
unsigned int i_orig_height;
unsigned int i_orig_width;
unsigned int i_origin_x;
unsigned int i_origin_y;
unsigned int i_scale_h;
unsigned int i_scale_v;
unsigned int i_alpha;
vlc_bool_t b_smooth;
mtime_t i_fade_in;
mtime_t i_fade_out;
unsigned int i_align;
mtime_t i_time_offset;
vlc_bool_t b_forced_subs;
unsigned int palette[16];
unsigned int colors[4];
} subtitle_data_t;
#endif /* "codecs.h" */
......@@ -2,7 +2,7 @@
* subsdec.c : text subtitles decoder
*****************************************************************************
* Copyright (C) 2000-2001 VideoLAN
* $Id: subsdec.c,v 1.3 2003/10/11 14:08:58 hartman Exp $
* $Id: subsdec.c,v 1.4 2003/11/05 00:17:50 hartman Exp $
*
* Authors: Gildas Bazin <gbazin@netcourrier.com>
* Samuel Hocevar <sam@zoy.org>
......@@ -32,6 +32,7 @@
#include <vlc/vout.h>
#include <vlc/decoder.h>
#include <osd.h>
#include <codecs.h>
#if defined(HAVE_ICONV)
#include <iconv.h>
......@@ -118,7 +119,8 @@ static int OpenDecoder( vlc_object_t *p_this )
{
decoder_t *p_dec = (decoder_t*)p_this;
if( p_dec->p_fifo->i_fourcc != VLC_FOURCC('s','u','b','t') )
if( p_dec->p_fifo->i_fourcc != VLC_FOURCC('s','u','b','t') &&
p_dec->p_fifo->i_fourcc != VLC_FOURCC('s','s','a',' ') )
{
return VLC_EGENERIC;
}
......@@ -144,6 +146,7 @@ static int OpenDecoder( vlc_object_t *p_this )
static int InitDecoder( decoder_t *p_dec )
{
decoder_sys_t *p_sys = p_dec->p_sys;
subtitle_data_t *p_demux_data = (subtitle_data_t *)p_dec->p_fifo->p_demux_data;
vlc_value_t val;
var_Create( p_dec, "subsdec-align", VLC_VAR_INTEGER | VLC_VAR_DOINHERIT );
......@@ -176,6 +179,11 @@ static int InitDecoder( decoder_t *p_dec )
msg_Dbg( p_dec, "No iconv support available" );
#endif
#if 1
if( p_demux_data )
msg_Dbg( p_dec, p_demux_data->psz_header );
#endif
return VLC_SUCCESS;
}
......@@ -250,6 +258,7 @@ static void ParseText( decoder_t *p_dec, block_t *p_block,
{
decoder_sys_t *p_sys = p_dec->p_sys;
char *psz_subtitle;
int i_align_h, i_align_v;
/* We cannot display a subpicture with no date */
if( p_block->i_pts == 0 )
......@@ -267,6 +276,9 @@ static void ParseText( decoder_t *p_dec, block_t *p_block,
/* Should be resiliant against bad subtitles */
psz_subtitle = strndup( p_block->p_buffer, p_block->i_buffer );
i_align_h = p_sys->i_align ? 20 : 0;
i_align_v = 10;
#if defined(HAVE_ICONV)
if( p_sys->iconv_handle != (iconv_t)-1 )
......@@ -297,9 +309,64 @@ static void ParseText( decoder_t *p_dec, block_t *p_block,
}
#endif
if( p_dec->p_fifo->i_fourcc == VLC_FOURCC('s','s','a',' ') )
{
/* Decode SSA strings */
/* We expect: ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text */
char *psz_new_subtitle;
char *psz_buffer_sub;
int i_comma;
int i_text;
psz_buffer_sub = psz_subtitle;
for( ;; )
{
i_comma = 0;
while( i_comma < 8 &&
*psz_buffer_sub != '\0' )
{
if( *psz_buffer_sub == ',' )
{
i_comma++;
}
psz_buffer_sub++;
}
psz_new_subtitle = malloc( strlen( psz_buffer_sub ) + 1);
i_text = 0;
while( psz_buffer_sub[0] != '\0' )
{
if( psz_buffer_sub[0] == '\\' && ( psz_buffer_sub[1] =='n' || psz_buffer_sub[1] =='N' ) )
{
psz_new_subtitle[i_text] = '\n';
i_text++;
psz_buffer_sub += 2;
}
else if( psz_buffer_sub[0] == '{' && psz_buffer_sub[1] == '\\' )
{
/* SSA control code */
while( psz_buffer_sub[0] != '\0' && psz_buffer_sub[0] != '}' )
{
psz_buffer_sub++;
}
psz_buffer_sub++;
}
else
{
psz_new_subtitle[i_text] = psz_buffer_sub[0];
i_text++;
psz_buffer_sub++;
}
}
psz_new_subtitle[i_text] = '\0';
free( psz_subtitle );
psz_subtitle = psz_new_subtitle;
break;
}
}
vout_ShowTextAbsolute( p_vout, psz_subtitle, NULL,
OSD_ALIGN_BOTTOM | p_sys->i_align,
p_sys->i_align ? 20 : 0, 10,
i_align_h, i_align_v,
p_block->i_pts, p_block->i_dts );
free( psz_subtitle );
......
......@@ -2,7 +2,7 @@
* mkv.cpp : matroska demuxer
*****************************************************************************
* Copyright (C) 2001 VideoLAN
* $Id: mkv.cpp,v 1.35 2003/11/02 18:03:45 sigmunau Exp $
* $Id: mkv.cpp,v 1.36 2003/11/05 00:17:50 hartman Exp $
*
* Authors: Laurent Aimar <fenrir@via.ecp.fr>
*
......@@ -1248,7 +1248,9 @@ static int Open( vlc_object_t * p_this )
!strcmp( tk.psz_codec, "S_SSA" ) ||
!strcmp( tk.psz_codec, "S_ASS" ))
{
tk.i_codec = VLC_FOURCC( 's', 'u', 'b', 't' );
tk.i_codec = VLC_FOURCC( 's', 's', 'a', ' ' );
tk.p_es->p_demux_data = malloc( sizeof( subtitle_data_t ) );
tk.p_es->p_demux_data->psz_header = strdup( (char *)tk.p_extra_data );
}
else if( !strcmp( tk.psz_codec, "S_VOBSUB" ) )
{
......@@ -1627,8 +1629,7 @@ static void BlockDecode( input_thread_t *p_input, KaxBlock *block, mtime_t i_pts
{
if( i_duration > 0 )
{
/* FIXME not sure about that */
p_pes->i_dts += i_duration * 1000;// * (mtime_t) 1000 / p_sys->i_timescale;
p_pes->i_dts += i_duration * 1000;
}
else
{
......@@ -1639,49 +1640,6 @@ static void BlockDecode( input_thread_t *p_input, KaxBlock *block, mtime_t i_pts
{
p_pes->p_first->p_payload_end[0] = '\0';
}
if( !strcmp( tk.psz_codec, "S_TEXT/SSA" ) ||
!strcmp( tk.psz_codec, "S_SSA" ) ||
!strcmp( tk.psz_codec, "S_ASS" ))
{
/* remove all fields before text for now */
char *start = (char*)p_pes->p_first->p_payload_start;
char *src, *dst;
int i_comma = 0;
while( *start && i_comma < 8 )
{
if( *start++ == ',' )
{
i_comma++;
}
}
memmove( p_pes->p_first->p_payload_start, start,
strlen( start) + 1 );
/* Fix the SSA string */
src = dst = (char*)p_pes->p_first->p_payload_start;
while( *src )
{
if( src[0] == '\\' && ( src[1] == 'n' || src[1] == 'N' ) )
{
dst[0] = '\n'; dst++; src += 2;
}
else if( src[0] == '{' && src[1] == '\\' )
{
while( *src && src[0] != '}' )
{
src++;
}
src++;
}
else
{
dst[0] = src[0]; dst++; src++;
}
}
dst++;
dst[0] = '\0';
}
}
input_DecodePES( tk.p_es->p_decoder_fifo, p_pes );
......
......@@ -2,7 +2,7 @@
* sub.c
*****************************************************************************
* Copyright (C) 1999-2003 VideoLAN
* $Id: sub.c,v 1.33 2003/11/04 11:11:30 titer Exp $
* $Id: sub.c,v 1.34 2003/11/05 00:17:50 hartman Exp $
*
* Authors: Laurent Aimar <fenrir@via.ecp.fr>
*
......@@ -32,6 +32,7 @@
#include <vlc/vlc.h>
#include <vlc/input.h>
#include "vlc_video.h"
#include <codecs.h>
#include "sub.h"
......@@ -204,26 +205,25 @@ static void text_rewind( text_t *txt )
txt->i_line = 0;
}
static int sub_MicroDvdRead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_SubRipRead ( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_SSA1Read ( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_SSA2_4Read ( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_Vplayer ( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_Sami ( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_VobSub ( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_MicroDvdRead( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_SubRipRead ( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_SSARead ( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_Vplayer ( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_Sami ( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static int sub_VobSub ( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe );
static struct
{
char *psz_type_name;
int i_type;
char *psz_name;
int (*pf_read_subtitle) ( text_t *, subtitle_t*, mtime_t );
int (*pf_read_subtitle) ( subtitle_demux_t *, text_t *, subtitle_t*, mtime_t );
} sub_read_subtitle_function [] =
{
{ "microdvd", SUB_TYPE_MICRODVD, "MicroDVD", sub_MicroDvdRead },
{ "subrip", SUB_TYPE_SUBRIP, "SubRIP", sub_SubRipRead },
{ "ssa1", SUB_TYPE_SSA1, "SSA-1", sub_SSA1Read },
{ "ssa2-4", SUB_TYPE_SSA2_4, "SSA-2/3/4",sub_SSA2_4Read },
{ "ssa1", SUB_TYPE_SSA1, "SSA-1", sub_SSARead },
{ "ssa2-4", SUB_TYPE_SSA2_4, "SSA-2/3/4",sub_SSARead },
{ "vplayer", SUB_TYPE_VPLAYER, "VPlayer", sub_Vplayer },
{ "sami", SUB_TYPE_SAMI, "SAMI", sub_Sami },
{ "vobsub", SUB_TYPE_VOBSUB, "VobSub", sub_VobSub },
......@@ -269,7 +269,7 @@ static int sub_open ( subtitle_demux_t *p_sub,
int i;
int i_sub_type;
int i_max;
int (*pf_read_subtitle)( text_t *, subtitle_t *, mtime_t ) = NULL;
int (*pf_read_subtitle)( subtitle_demux_t *, text_t *, subtitle_t *, mtime_t ) = NULL;
p_sub->i_sub_type = SUB_TYPE_UNKNOWN;
p_sub->p_es = NULL;
......@@ -433,15 +433,23 @@ static int sub_open ( subtitle_demux_t *p_sub,
i_max += 128;
if( p_sub->subtitle )
{
p_sub->subtitle = realloc( p_sub->subtitle,
sizeof( subtitle_t ) * i_max );
if( !( p_sub->subtitle = realloc( p_sub->subtitle,
sizeof( subtitle_t ) * i_max ) ) )
{
msg_Err( p_sub, "out of memory");
return VLC_ENOMEM;
}
}
else
{
p_sub->subtitle = malloc( sizeof( subtitle_t ) * i_max );
if( !( p_sub->subtitle = malloc( sizeof( subtitle_t ) * i_max ) ) )
{
msg_Err( p_sub, "out of memory");
return VLC_ENOMEM;
}
}
}
if( pf_read_subtitle( &txt,
if( pf_read_subtitle( p_sub, &txt,
p_sub->subtitle + p_sub->i_subtitles,
i_microsecperframe ) < 0 )
{
......@@ -456,7 +464,10 @@ static int sub_open ( subtitle_demux_t *p_sub,
/* *** fix subtitle (order and time) *** */
p_sub->i_subtitle = 0; /* will be modified by sub_fix */
sub_fix( p_sub );
if( p_sub->i_sub_type != SUB_TYPE_VOBSUB )
{
sub_fix( p_sub );
}
/* *** add subtitle ES *** */
vlc_mutex_lock( &p_input->stream.stream_lock );
......@@ -466,16 +477,28 @@ static int sub_open ( subtitle_demux_t *p_sub,
vlc_mutex_unlock( &p_input->stream.stream_lock );
p_sub->p_es->i_stream_id = 0xff - i_track_id; /* FIXME */
if( p_sub->i_sub_type != SUB_TYPE_VOBSUB )
if( p_sub->psz_header != NULL )
{
p_sub->p_es->i_fourcc = VLC_FOURCC( 's','u','b','t' );
p_sub->p_es->p_demux_data = malloc( sizeof( subtitle_data_t ) );
p_sub->p_es->p_demux_data->psz_header = strdup( p_sub->psz_header );
free( p_sub->psz_header );
}
else
if( p_sub->i_sub_type == SUB_TYPE_VOBSUB )
{
p_sub->p_es->i_fourcc = VLC_FOURCC( 's','p','u',' ' );
/* open vobsub file */
}
else if( p_sub->i_sub_type == SUB_TYPE_SSA1 ||
p_sub->i_sub_type == SUB_TYPE_SSA2_4 )
{
p_sub->p_es->i_fourcc = VLC_FOURCC( 's','s','a',' ' );
}
else
{
p_sub->p_es->i_fourcc = VLC_FOURCC( 's','u','b','t' );
}
p_sub->i_previously_selected = 0;
return VLC_SUCCESS;
......@@ -605,8 +628,8 @@ static void sub_close( subtitle_demux_t *p_sub )
free( p_sub->subtitle );
}
}
/*****************************************************************************
*
* sub_fix: fix time stamp and order of subtitle
*****************************************************************************/
static void sub_fix( subtitle_demux_t *p_sub )
......@@ -666,7 +689,7 @@ static void sub_fix( subtitle_demux_t *p_sub )
/*****************************************************************************
* Specific Subtitle function
*****************************************************************************/
static int sub_MicroDvdRead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe)
static int sub_MicroDvdRead( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe)
{
/*
* each line:
......@@ -711,7 +734,7 @@ static int sub_MicroDvdRead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_mic
return( 0 );
}
static int sub_SubRipRead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe )
static int sub_SubRipRead( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe )
{
/*
* n
......@@ -762,7 +785,7 @@ static int sub_SubRipRead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_micro
i_len = strlen( s );
if( i_len <= 1 )
{
// empty line -> end of this subtitle
/* empty line -> end of this subtitle */
buffer_text[__MAX( i_buffer_text - 1, 0 )] = '\0';
p_subtitle->i_start = i_start;
p_subtitle->i_stop = i_stop;
......@@ -788,15 +811,12 @@ static int sub_SubRipRead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_micro
}
static int sub_SSARead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe, int i_comma_count )
static int sub_SSARead( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe )
{
char buffer_text[ 10 * MAX_LINE];
char *s;
char *p_buffer_text;
mtime_t i_start;
mtime_t i_stop;
int i_comma;
int i_text;
for( ;; )
{
......@@ -807,8 +827,10 @@ static int sub_SSARead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsec
{
return( VLC_EGENERIC );
}
p_subtitle->psz_text = malloc( strlen( s ) );
if( sscanf( s,
"Dialogue: Marked=%d,%d:%d:%d.%d,%d:%d:%d.%d,%[^\r\n]",
"Dialogue: Marked=%d,%d:%d:%d.%d,%d:%d:%d.%d%[^\r\n]",
&i_dummy,
&h1, &m1, &s1, &c1,
&h2, &m2, &s2, &c2,
......@@ -824,61 +846,48 @@ static int sub_SSARead( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsec
(mtime_t)s2 * 1000 +
(mtime_t)c2 * 10 ) * 1000;
p_buffer_text = buffer_text;
i_comma = 3;
while( i_comma < i_comma_count &&
*p_buffer_text != '\0' )
/* The dec expects: ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text */
if( p_sub->i_sub_type == SUB_TYPE_SSA1 )
{
if( *p_buffer_text == ',' )
{
i_comma++;
}
p_buffer_text++;
sprintf( p_subtitle->psz_text, ",%d%s", i_dummy, strdup( buffer_text) );
}
else
{
sprintf( p_subtitle->psz_text, ",%d,%s", i_dummy, strdup( buffer_text) );
}
p_subtitle->psz_text = malloc( strlen( p_buffer_text ) + 1);
i_text = 0;
while( p_buffer_text[0] != '\0' )
p_subtitle->i_start = i_start;
p_subtitle->i_stop = i_stop;
return( 0 );
}
else
{
/* All the other stuff we add to the header field */
if( p_sub->psz_header != NULL )
{
if( p_buffer_text[0] == '\\' && ( p_buffer_text[1] =='n' || p_buffer_text[1] =='N' ) )
{
p_subtitle->psz_text[i_text] = '\n';
i_text++;
p_buffer_text += 2;
}
else if( p_buffer_text[0] == '{' && p_buffer_text[1] == '\\' )
if( !( p_sub->psz_header = realloc( p_sub->psz_header,
strlen( p_sub->psz_header ) + strlen( s ) + 2 ) ) )
{
/* SSA control code */
while( p_buffer_text[0] != '\0' && p_buffer_text[0] != '}' )
{
p_buffer_text++;
}
p_buffer_text++;
msg_Err( p_sub, "out of memory");
return VLC_ENOMEM;
}
else
p_sub->psz_header = strcat( p_sub->psz_header, strdup( s ) );
p_sub->psz_header = strcat( p_sub->psz_header, "\n" );
}
else
{
if( !( p_sub->psz_header = malloc( strlen( s ) + 2 ) ) )
{
p_subtitle->psz_text[i_text] = p_buffer_text[0];
i_text++;
p_buffer_text++;
msg_Err( p_sub, "out of memory");
return VLC_ENOMEM;
}
p_sub->psz_header = strdup( s );
p_sub->psz_header = strcat( p_sub->psz_header, "\n" );
}
p_subtitle->psz_text[++i_text] = '\0';
p_subtitle->i_start = i_start;
p_subtitle->i_stop = i_stop;
return( 0 );
}
}
}
static int sub_SSA1Read( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe )
{
return( sub_SSARead( txt, p_subtitle, i_microsecperframe, 8 ) );
}
static int sub_SSA2_4Read( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe )
{
return( sub_SSARead( txt, p_subtitle, i_microsecperframe, 9 ) );
}
static int sub_Vplayer( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe)
static int sub_Vplayer( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe)
{
/*
* each line:
......@@ -961,7 +970,7 @@ static char *sub_SamiSearch( text_t *txt, char *psz_start, char *psz_str )
}
}
static int sub_Sami( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe )
static int sub_Sami( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe )
{
char *p;
int i_start;
......@@ -1050,7 +1059,7 @@ static int sub_Sami( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecper
#undef ADDC
}
static int sub_VobSub( text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe)
static int sub_VobSub( subtitle_demux_t *p_sub, text_t *txt, subtitle_t *p_subtitle, mtime_t i_microsecperframe)
{
/*
* Parse the idx file. Each line:
......
......@@ -2,7 +2,7 @@
* sub.h
*****************************************************************************
* Copyright (C) 2001-2003 VideoLAN
* $Id: sub.h,v 1.9 2003/10/31 22:46:19 hartman Exp $
* $Id: sub.h,v 1.10 2003/11/05 00:17:50 hartman Exp $
*
* Authors: Laurent Aimar <fenrir@via.ecp.fr>
*
......@@ -43,6 +43,7 @@ typedef struct subtitle_s
typedef struct subtitle_track_s
{
int i_track_id;
char *psz_header;
int i_subtitle;
int i_subtitles;
subtitle_t *subtitle;
......@@ -73,12 +74,13 @@ typedef struct subtitle_demux_s
input_thread_t *p_input;
int i_sub_type;
int i_previously_selected; /* to make pf_seek */
char *psz_header;
int i_subtitle;
int i_subtitles;
subtitle_t *subtitle;
es_descriptor_t *p_es;
int i_previously_selected; /* to make pf_seek */
/*unsigned int i_tracks;
subtitle_track_t *p_tracks
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment