diff --git a/Makefile.am b/Makefile.am index 87f93f834..74241fc85 100644 --- a/Makefile.am +++ b/Makefile.am @@ -137,7 +137,9 @@ pkglib_LTLIBRARIES += libsubtext.la libsubtext_la_SOURCES = src/filters/subtext/text.c \ src/filters/subtext/common.c \ src/filters/subtext/common.h \ - src/filters/subtext/image.cpp + src/filters/subtext/image.cpp \ + src/filters/subtext/toass.cpp \ + src/filters/subtext/toutf8.c libsubtext_la_LDFLAGS = $(commonpluginldflags) libsubtext_la_LIBTOOLFLAGS = $(commonlibtoolflags) diff --git a/configure.ac b/configure.ac index 435410ff5..7e90b23d5 100644 --- a/configure.ac +++ b/configure.ac @@ -12,6 +12,10 @@ AC_PROG_CC AC_PROG_CXX +AC_SYS_LARGEFILE +AC_FUNC_FSEEKO + + AC_ARG_ENABLE([debug], AS_HELP_STRING([--enable-debug], [Enable compilation options required for debugging. (default=no)])) AS_IF( [test "x$enable_debug" = "xyes"], @@ -360,6 +364,32 @@ AS_CASE( ] ) +AC_CHECK_HEADERS( + [iconv.h], + [], + [ + AS_IF( + [test "x$subtext" = "xyes"], + [AC_MSG_ERROR([the subtext plugin was explicitly enabled, but iconv.h cannot be found.])], + [subtext=""] + ) + ] + ) + +AC_SEARCH_LIBS([libiconv_open], [iconv]) +AC_SEARCH_LIBS([iconv_open], [iconv]) + +AS_IF( + [test "x$ac_cv_search_libiconv_open" = "xno" -a "x$ac_cv_search_iconv_open" = "xno"], + [ + AS_IF( + [test "x$subtext" = "xyes"], + [AC_MSG_ERROR([the subtext plugin was explicitly enabled, but iconv_open cannot be found.])], + [subtext=""] + ) + ] + ) + PKG_CHECK_MODULES( [LIBASS], [libass], diff --git a/doc/installation.rst b/doc/installation.rst index cac56c115..711a259d9 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -75,7 +75,7 @@ These are the requirements: * Sphinx for the documentation (optional) - * libass and ffmpeg for the Subtext plugin (optional) + * iconv, libass, and ffmpeg for the Subtext plugin (optional) * ImageMagick 7 for the Imwri plugin (optional) diff --git a/doc/plugins/subtext.rst b/doc/plugins/subtext.rst index f1b337b97..541b590dd 100644 --- a/doc/plugins/subtext.rst +++ b/doc/plugins/subtext.rst @@ -5,10 +5,10 @@ Subtext Subtext is a subtitle renderer that uses libass and ffmpeg. -.. function:: TextFile(clip clip, string file[, string charset="UTF-8", float scale=1, int debuglevel=0, string fontdir="", float linespacing=0, int[] margins=[0, 0, 0, 0], float sar=0, bint blend=True, int matrix, string matrix_s, int transfer, string transfer_s, int primaries, string primaries_s]) +.. function:: TextFile(clip clip, string file[, string charset="UTF-8", float scale=1, int debuglevel=0, string fontdir="", float linespacing=0, int[] margins=[0, 0, 0, 0], float sar=0, string style="", bint blend=True, int matrix, string matrix_s, int transfer, string transfer_s, int primaries, string primaries_s]) :module: sub - TextFile renders ASS subtitles. + TextFile renders text subtitles, such as ASS and SRT. TextFile has two modes of operation. With blend=True (the default), it returns *clip* with the subtitles burned in. With blend=False, it @@ -22,10 +22,10 @@ Subtext is a subtitle renderer that uses libass and ffmpeg. Input clip. file - ASS script to be rendered. + Subtitle file to be rendered. charset - Character set of the ASS script, in enca or iconv format. + Character set of the subtitle, in iconv format. scale Font scale. @@ -48,6 +48,11 @@ Subtext is a subtitle renderer that uses libass and ffmpeg. sar Storage aspect ratio. + style + Custom ASS style for subtitle formats other than ASS. If empty + (the default), libavcodec's default style is used. This + parameter has no effect on ASS subtitles. + blend If True, the subtitles will be blended into *clip*. Otherwise, the bitmaps will be returned untouched. @@ -68,10 +73,10 @@ Subtext is a subtitle renderer that uses libass and ffmpeg. "709". -.. function:: Subtitle(clip clip, string text[, string style="sans-serif,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,7,10,10,10,1", int start=0, int end=clip.numFrames, int debuglevel=0, string fontdir="", float linespacing=0, int[] margins=[0, 0, 0, 0], float sar=0, bint blend=True, int matrix, string matrix_s, int transfer, string transfer_s, int primaries, string primaries_s]) +.. function:: Subtitle(clip clip, string text[, int start=0, int end=clip.numFrames, int debuglevel=0, string fontdir="", float linespacing=0, int[] margins=[0, 0, 0, 0], float sar=0, string style="sans-serif,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,7,10,10,10,1", bint blend=True, int matrix, string matrix_s, int transfer, string transfer_s, int primaries, string primaries_s]) :module: sub - Instead of rendering an ASS script, Subtitle renders the string *text*. + Instead of rendering a subtitle file, Subtitle renders the string *text*. Otherwise it works the same as TextFile. Parameters: diff --git a/src/filters/subtext/text.c b/src/filters/subtext/text.c index c9ec7b775..159599bf9 100644 --- a/src/filters/subtext/text.c +++ b/src/filters/subtext/text.c @@ -266,6 +266,11 @@ static int frameToTime(int frame, int64_t fpsNum, int64_t fpsDen, char *str, siz return 1; } + +char *convertToUtf8(const char *file_name, const char *charset, int64_t *file_size, char *error, size_t error_size); +ASS_Track *convertToASS(const char *file_name, const char *contents, size_t contents_size, ASS_Library *ass_library, const char *user_style, const char *charset, char *error, size_t error_size); + + static void VS_CC assRenderCreate(const VSMap *in, VSMap *out, void *userData, VSCore *core, const VSAPI *vsapi) { @@ -298,12 +303,6 @@ static void VS_CC assRenderCreate(const VSMap *in, VSMap *out, void *userData, d.file = NULL; d.text = vsapi->propGetData(in, "text", 0, &err); - d.style = vsapi->propGetData(in, "style", 0, &err); - - if(err) { - d.style = "sans-serif,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,7,10,10,10,1"; - } - d.startframe = int64ToIntS(vsapi->propGetInt(in, "start", 0, &err)); if (err) { d.startframe = 0; @@ -325,6 +324,12 @@ static void VS_CC assRenderCreate(const VSMap *in, VSMap *out, void *userData, } } + d.style = vsapi->propGetData(in, "style", 0, &err); + + if(err && !d.file) { + d.style = "sans-serif,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,7,10,10,10,1"; + } + d.charset = vsapi->propGetData(in, "charset", 0, &err); if(err) @@ -456,16 +461,27 @@ static void VS_CC assRenderCreate(const VSMap *in, VSMap *out, void *userData, free(str); } else { - d.ass = ass_read_file(d.ass_library, (char *)d.file, (char *)d.charset); - } + snprintf(error, ERROR_SIZE, "%s: ", filter_name); - if(!d.ass) { - snprintf(error, ERROR_SIZE, "%s: unable to parse input file", filter_name); - vsapi->setError(out, error); - vsapi->freeNode(d.node); - ass_renderer_done(d.ass_renderer); - ass_library_done(d.ass_library); - return; + int64_t contents_size; + char *contents = convertToUtf8(d.file, d.charset, &contents_size, error + strlen(error), ERROR_SIZE - strlen(error)); + + if (contents) { + d.ass = ass_read_memory(d.ass_library, contents, contents_size, NULL); + + if (!d.ass) + d.ass = convertToASS(d.file, contents, contents_size, d.ass_library, d.style, d.charset, error + strlen(error), ERROR_SIZE - strlen(error)); + + free(contents); + } + + if (!contents || !d.ass) { + vsapi->setError(out, error); + vsapi->freeNode(d.node); + ass_renderer_done(d.ass_renderer); + ass_library_done(d.ass_library); + return; + } } d.lastframe = vsapi->newVideoFrame(d.vi[0].format, @@ -526,7 +542,8 @@ VS_EXTERNAL_API(void) VapourSynthPluginInit(VSConfigPlugin configFunc, "fontdir:data:opt;" \ "linespacing:float:opt;" \ "margins:int[]:opt;" \ - "sar:float:opt;" + "sar:float:opt;" \ + "style:data:opt;" #define COMMON_PARAMS \ "blend:int:opt;" \ @@ -548,7 +565,6 @@ VS_EXTERNAL_API(void) VapourSynthPluginInit(VSConfigPlugin configFunc, registerFunc("Subtitle", "clip:clip;" "text:data;" - "style:data:opt;" "start:int:opt;" "end:int:opt;" COMMON_TEXTFILE_PARAMS diff --git a/src/filters/subtext/toass.cpp b/src/filters/subtext/toass.cpp new file mode 100644 index 000000000..894422c6d --- /dev/null +++ b/src/filters/subtext/toass.cpp @@ -0,0 +1,274 @@ + +#include +#include +#include + +#include +#include + +extern "C" { +#include +#include +} + + +struct MemoryFile { + const char *data; + int64_t total_size; + int64_t current_position; + + + static int64_t seek(void *opaque, int64_t offset, int whence); + + static int readPacket(void *opaque, uint8_t *buf, int bytes_to_read); +}; + + +int64_t MemoryFile::seek(void *opaque, int64_t offset, int whence) { + if (whence & AVSEEK_FORCE) + whence &= ~AVSEEK_FORCE; + + MemoryFile *mf = (MemoryFile *)opaque; + + if (whence == AVSEEK_SIZE) { + return mf->total_size; + } else if (whence == SEEK_SET) { + } else if (whence == SEEK_CUR) { + offset += mf->current_position; + } else if (whence == SEEK_END) { + offset += mf->total_size; + } else { + return -1; + } + + mf->current_position = offset; + return mf->current_position; +} + + +int MemoryFile::readPacket(void *opaque, uint8_t *buf, int bytes_to_read) { + MemoryFile *mf = (MemoryFile *)opaque; + + int bytes_read = std::min((int64_t)bytes_to_read, mf->total_size - mf->current_position); + + memcpy(buf, mf->data + mf->current_position, bytes_read); + + mf->current_position += bytes_read; + + return bytes_read; +} + + +extern "C" ASS_Track *convertToASS(const char *file_name, const char *contents, size_t contents_size, ASS_Library *ass_library, const char *user_style, const char *charset, char *error, size_t error_size) { + av_log_set_level(AV_LOG_PANIC); /// would be good to have a parameter for this + av_register_all(); + avcodec_register_all(); + + MemoryFile memory_file = { }; + memory_file.data = contents; + memory_file.total_size = contents_size; + + AVFormatContext *fctx = nullptr; + uint8_t *avio_buffer = nullptr; + + try { + fctx = avformat_alloc_context(); + if (!fctx) + throw std::string("failed to allocate AVFormatContext."); + + const int avio_buffer_size = 4 * 1024; + + avio_buffer = (uint8_t *)av_malloc(avio_buffer_size); + if (!avio_buffer) + throw std::string("failed to allocate buffer for AVIOContext."); + + fctx->pb = avio_alloc_context(avio_buffer, avio_buffer_size, 0, &memory_file, MemoryFile::readPacket, nullptr, MemoryFile::seek); + if (!fctx->pb) + throw std::string("failed to allocate AVIOContext."); + } catch (const std::string &e) { + snprintf(error, error_size, "%s", e.c_str()); + + if (!fctx || !fctx->pb) + av_freep(&avio_buffer); + + if (fctx) + avformat_free_context(fctx); + + return nullptr; + } + + int ret = 0; + + try { + ret = avformat_open_input(&fctx, file_name, nullptr, nullptr); + if (ret < 0) + throw std::string("avformat_open_input failed: "); + + ret = avformat_find_stream_info(fctx, nullptr); + if (ret < 0) + throw std::string("avformat_find_stream_info failed: "); + } catch (const std::string &e) { + char av_error[AV_ERROR_MAX_STRING_SIZE] = { 0 }; + + snprintf(error, error_size, "%s%s.", e.c_str(), av_strerror(ret, av_error, AV_ERROR_MAX_STRING_SIZE) ? strerror(ret) : av_error); + + avformat_close_input(&fctx); + + return nullptr; + } + + if (fctx->nb_streams == 0) { + snprintf(error, error_size, "no streams found."); + + avformat_close_input(&fctx); + + return nullptr; + } + + int stream_index = 0; + + enum AVCodecID codec_id = fctx->streams[stream_index]->codec->codec_id; + + AVCodecContext *avctx = nullptr; + + ret = 0; + + try { + const AVCodecDescriptor *descriptor = avcodec_descriptor_get(codec_id); + if (descriptor->type != AVMEDIA_TYPE_SUBTITLE || !(descriptor->props & AV_CODEC_PROP_TEXT_SUB)) + throw std::string("file is not a text subtitle."); + + AVCodec *decoder = avcodec_find_decoder(codec_id); + if (!decoder) + throw std::string("failed to find decoder for '") + avcodec_get_name(codec_id) + "'."; + + avctx = avcodec_alloc_context3(decoder); + if (!avctx) + throw std::string("failed to allocate AVCodecContext."); + + int extradata_size = fctx->streams[stream_index]->codec->extradata_size; + if (extradata_size) { + avctx->extradata_size = extradata_size; + avctx->extradata = (uint8_t *)av_mallocz(extradata_size + AV_INPUT_BUFFER_PADDING_SIZE); + if (!avctx->extradata) + throw std::string("failed to allocate extradata."); + + memcpy(avctx->extradata, fctx->streams[stream_index]->codec->extradata, extradata_size); + } + + ret = avcodec_open2(avctx, decoder, NULL); + if (ret < 0) + throw std::string("failed to open AVCodecContext."); + + if (!avctx->subtitle_header_size) + throw std::string("no subtitle header found in AVCodecContext."); + } catch (const std::string &e) { + if (avctx) + avcodec_free_context(&avctx); + + avformat_close_input(&fctx); + + snprintf(error, error_size, "%s", e.c_str()); + + return nullptr; + } + + avctx->time_base = av_make_q(1, 1000); + avctx->pkt_timebase = avctx->time_base; + + + std::string ass_file; + + if (user_style) { + std::string subtitle_header((char *)avctx->subtitle_header, avctx->subtitle_header_size); + + size_t style_position = subtitle_header.find("Style:"); + + if (style_position == std::string::npos) { + snprintf(error, error_size, "subtitle header in AVCodecContext has no styles."); + + avcodec_free_context(&avctx); + avformat_close_input(&fctx); + + return nullptr; + } + + size_t events_position = subtitle_header.find("[Events]"); + + if (events_position == std::string::npos) { + snprintf(error, error_size, "subtitle header in AVCodecContext has no [Events] section."); + + avcodec_free_context(&avctx); + avformat_close_input(&fctx); + + return nullptr; + } + + size_t events_size = avctx->subtitle_header_size - events_position; + + ass_file.append((char *)avctx->subtitle_header, style_position); + ass_file.append("Style: Default,"); + ass_file.append(user_style); + ass_file.append("\n"); + ass_file.append((char *)avctx->subtitle_header + events_position, events_size); + } else { + ass_file.append((char *)avctx->subtitle_header, avctx->subtitle_header_size); + } + + + AVPacket packet; + av_init_packet(&packet); + + AVSubtitle avsub; + + int total_events = 0; + int failed_decoding = 0; + + while (av_read_frame(fctx, &packet) == 0) { + if (packet.stream_index != stream_index) { + av_packet_unref(&packet); + continue; + } + + total_events++; + + int got_avsub = 0; + + AVPacket decoded_packet = packet; + + ret = avcodec_decode_subtitle2(avctx, &avsub, &got_avsub, &decoded_packet); + if (ret < 0 || !got_avsub) { + av_packet_unref(&packet); + failed_decoding++; + continue; + } + + for (unsigned i = 0; i < avsub.num_rects; i++) { + AVSubtitleRect *rect = avsub.rects[i]; + + if (rect->type != SUBTITLE_ASS || !rect->ass || !rect->ass[0]) { + avsubtitle_free(&avsub); + av_packet_unref(&packet); + continue; + } + + ass_file += rect->ass; + } + + avsubtitle_free(&avsub); + av_packet_unref(&packet); + } + + avcodec_free_context(&avctx); + avformat_close_input(&fctx); + + if (failed_decoding > 0) { + snprintf(error, error_size, "failed to decode %d events out of %d. Are you sure '%s' is the correct charset?", failed_decoding, total_events, charset); + + return nullptr; + } + + ASS_Track *track = ass_read_memory(ass_library, (char *)ass_file.c_str(), ass_file.size(), nullptr); + + return track; +} diff --git a/src/filters/subtext/toutf8.c b/src/filters/subtext/toutf8.c new file mode 100644 index 000000000..c0e5ef885 --- /dev/null +++ b/src/filters/subtext/toutf8.c @@ -0,0 +1,211 @@ +#ifdef _MSC_VER +#undef fseeko +#undef ftello +#define fseeko _fseeki64 +#define ftello _ftelli64 +#else +// To shut up the "implicit declaration of fseeko" warning. +#define _POSIX_C_SOURCE 200112L +#endif + +#include +#include +#include +#include +#include +#include + +#include + +#ifdef _WIN32 +#include +#endif + + + +// Function stolen from libass. +static char *sub_recode(char *data, size_t *size, const char *codepage, char *error, size_t error_size) { + iconv_t icdsc; + char *tocp = "UTF-8"; + char *outbuf; + + if ((icdsc = iconv_open(tocp, codepage)) == (iconv_t) (-1)) { + snprintf(error, error_size, "failed to open iconv descriptor."); + + return NULL; + } + + { + size_t osize = *size; + size_t ileft = *size; + size_t oleft = *size - 1; + char *ip; + char *op; + size_t rc; + int clear = 0; + + outbuf = malloc(osize); + if (!outbuf) { + snprintf(error, error_size, "failed to allocate %zu bytes for iconv.", osize); + + goto out; + } + + ip = data; + op = outbuf; + + while (1) { + if (ileft) + rc = iconv(icdsc, &ip, &ileft, &op, &oleft); + else { // clear the conversion state and leave + clear = 1; + rc = iconv(icdsc, NULL, NULL, &op, &oleft); + } + if (rc == (size_t) (-1)) { + if (errno == E2BIG) { + size_t offset = op - outbuf; + char *nbuf = realloc(outbuf, osize + *size); + if (!nbuf) { + free(outbuf); + outbuf = NULL; + snprintf(error, error_size, "failed to reallocate %zu bytes for iconv.", osize + *size); + goto out; + } + outbuf = nbuf; + op = outbuf + offset; + osize += *size; + oleft += *size; + } else { + snprintf(error, error_size, "failed to convert subtitle file to UTF8, at byte %zu.", (size_t)(ip - data)); + free(outbuf); + outbuf = NULL; + goto out; + } + } else if (clear) + break; + } + outbuf[osize - oleft - 1] = 0; + *size = osize - oleft - 1; + } + +out: + if (icdsc != (iconv_t) (-1)) { + (void) iconv_close(icdsc); + } + + return outbuf; +} + + +static int isUtf8(const char *charset) { + size_t length = strlen(charset); + if (length != 4 && length != 5) + return 0; + + char charset_upper[10] = { 0 }; + + int diff = 'a' - 'A'; + for (size_t i = 0; i < length; i++) { + charset_upper[i] = charset[i]; + if (charset[i] >= 'a' && charset[i] <= 'z') + charset_upper[i] -= diff; + } + + return strcmp(charset_upper, "UTF8") == 0 || + strcmp(charset_upper, "UTF-8") == 0; +} + + +char *convertToUtf8(const char *file_name, const char *charset, int64_t *file_size, char *error, size_t error_size) { +#ifdef _WIN32 + int required_size = MultiByteToWideChar(CP_UTF8, 0, file_name, -1, NULL, 0); + wchar_t *wbuffer = malloc(required_size * sizeof(wchar_t)); + if (!wbuffer) { + snprintf(error, error_size, "failed to allocate %zu bytes for file name conversion for _wfopen.", required_size * sizeof(wchar_t)); + + return NULL; + } + if (MultiByteToWideChar(CP_UTF8, 0, file_name, -1, wbuffer, required_size) != required_size) { + free(wbuffer); + + snprintf(error, error_size, "file name conversion for _wfopen failed."); + + return NULL; + } + FILE *f = _wfopen(wbuffer, L"rb"); + free(wbuffer); +#else + FILE *f = fopen(file_name, "rb"); +#endif + + if (!f) { + snprintf(error, error_size, "failed to open subtitle file: %s.", strerror(errno)); + + return NULL; + } + + if (fseeko(f, 0, SEEK_END)) { + snprintf(error, error_size, "failed to seek to the end of the subtitle file: %s.", strerror(errno)); + + fclose(f); + + return NULL; + } + + *file_size = ftello(f); + if (*file_size == -1) { + snprintf(error, error_size, "failed to obtain the size of the subtitle file: %s.", strerror(errno)); + + fclose(f); + + return NULL; + } + + if (*file_size > 50 * 1024 * 1024) { + snprintf(error, error_size, "subtitle file size of %" PRId64 " bytes is unreasonably large.", *file_size); + + fclose(f); + + return NULL; + } + + if (fseeko(f, 0, SEEK_SET)) { + snprintf(error, error_size, "failed to seek back to the beginning of the subtitle file: %s.", strerror(errno)); + + fclose(f); + + return NULL; + } + + + char *contents = malloc(*file_size); + if (!contents) { + snprintf(error, error_size, "failed to allocate %" PRId64 " bytes for the contents of the subtitle file.", *file_size); + + fclose(f); + + return NULL; + } + + size_t bytes_read = fread(contents, 1, *file_size, f); + if (bytes_read != (size_t)*file_size) { + snprintf(error, error_size, "expected to read %" PRId64 " bytes, but read only %zu bytes.", *file_size, bytes_read); + + fclose(f); + free(contents); + + return NULL; + } + + fclose(f); + f = NULL; + + + if (charset && !isUtf8(charset)) { + char *utf8_contents = sub_recode(contents, (size_t *)file_size, charset, error, error_size); + free(contents); + contents = utf8_contents; + } + + return contents; +}