diff --git a/Makefile b/Makefile index 4819893..2b84add 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,41 @@ -PROJECT_NAME = dantto4k - -SRC_DIR = src -OBJ_DIR = build - -SRC_FILES = $(filter-out $(SRC_DIR)/bonTuner.cpp $(SRC_DIR)/dllmain.cpp, $(wildcard $(SRC_DIR)/*.cpp)) - -OBJ_FILES = $(SRC_FILES:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o) - -TSDUCK_INC = $(shell pkg-config --cflags-only-I tsduck) -TSDUCK_LIB = $(shell pkg-config --libs tsduck) - -PCSC_INC = $(shell pkg-config --cflags-only-I libpcsclite) -PCSC_LIB = $(shell pkg-config --libs libpcsclite) - -CXX = g++ -CXXFLAGS = -std=c++20 -Wall -maes -msse4.1 $(TSDUCK_INC) $(PCSC_INC) -Ithirdparty/asio/asio/include -LDFLAGS = $(TSDUCK_LIB) $(PCSC_LIB) - -EXEC = $(OBJ_DIR)/$(PROJECT_NAME) - -all: $(EXEC) - -$(OBJ_DIR): - mkdir -p $(OBJ_DIR) - -$(EXEC): $(OBJ_FILES) - $(CXX) $(OBJ_FILES) $(LDFLAGS) -o $(EXEC) - -$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR) - $(CXX) $(CXXFLAGS) -c $< -o $@ - -clean: - rm -rf $(OBJ_DIR) - -install: - cp $(EXEC) /usr/local/bin/$(PROJECT_NAME) - -.PHONY: all clean install \ No newline at end of file +PROJECT_NAME = dantto4k + +SRC_DIR = src +OBJ_DIR = build + +SRC_FILES = $(filter-out $(SRC_DIR)/bonTuner.cpp $(SRC_DIR)/dllmain.cpp, \ + $(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(SRC_DIR)/ttml/*.cpp)) + +OBJ_FILES = $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRC_FILES)) + +TSDUCK_INC = $(shell pkg-config --cflags-only-I tsduck) +TSDUCK_LIB = $(shell pkg-config --libs tsduck) + +PCSC_INC = $(shell pkg-config --cflags-only-I libpcsclite) +PCSC_LIB = $(shell pkg-config --libs libpcsclite) + +CXX = g++ +CXXFLAGS = -std=c++20 -Wall -maes -msse4.1 $(TSDUCK_INC) $(PCSC_INC) -I$(SRC_DIR) -Ithirdparty/asio/asio/include +LDFLAGS = $(TSDUCK_LIB) $(PCSC_LIB) + +EXEC = $(OBJ_DIR)/$(PROJECT_NAME) + +all: $(EXEC) + +$(OBJ_DIR): + mkdir -p $(OBJ_DIR) + +$(EXEC): $(OBJ_FILES) + $(CXX) $(OBJ_FILES) $(LDFLAGS) -o $(EXEC) + +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR) + mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJ_DIR) + +install: + cp $(EXEC) /usr/local/bin/$(PROJECT_NAME) + +.PHONY: all clean install diff --git a/msvc/dantto4k.vcxproj b/msvc/dantto4k.vcxproj index 944df95..7a34c2d 100644 --- a/msvc/dantto4k.vcxproj +++ b/msvc/dantto4k.vcxproj @@ -30,26 +30,26 @@ Application true - v143 + v145 Unicode Application false - v143 + v145 true Unicode Application true - v143 + v145 MultiByte Application false - v143 + v145 true MultiByte @@ -67,19 +67,19 @@ - - - - - - - - - $(Platform)\$(Configuration)\dantto4k\ - - - $(Platform)\$(Configuration)\dantto4k\ - + + + + + + + + + $(Platform)\$(Configuration)\dantto4k\ + + + $(Platform)\$(Configuration)\dantto4k\ + Level3 @@ -112,38 +112,40 @@ Level3 true - _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;_DEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) - true - stdcpp20 - MultiThreadedDebug - /FS %(AdditionalOptions) - + _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;_DEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreadedDebugDLL + /FS %(AdditionalOptions) + $(ProjectDir)../src;%(AdditionalIncludeDirectories) + Console true - %(AdditionalDependencies) - - + %(AdditionalDependencies) + + Level3 true true true - _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;NDEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) - true - stdcpp20 - MultiThreaded - /FS %(AdditionalOptions) - + _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;NDEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreaded + /FS %(AdditionalOptions) + $(ProjectDir)../src;%(AdditionalIncludeDirectories) + Console true true true - %(AdditionalDependencies) - - + %(AdditionalDependencies) + + @@ -165,7 +167,8 @@ - + + @@ -189,7 +192,8 @@ - + + @@ -198,7 +202,6 @@ - @@ -248,6 +251,10 @@ + + + + @@ -271,7 +278,8 @@ - + + @@ -301,7 +309,8 @@ - + + @@ -311,7 +320,6 @@ - @@ -372,8 +380,16 @@ + + + + + + + + - + diff --git a/msvc/dantto4k.vcxproj.filters b/msvc/dantto4k.vcxproj.filters index 6cae4f5..b0e8115 100644 --- a/msvc/dantto4k.vcxproj.filters +++ b/msvc/dantto4k.vcxproj.filters @@ -220,9 +220,12 @@ mmttlv\tlv\structs - - mmttlv\tlv\structs - + + mmttlv\tlv\structs + + + mmttlv\tlv\structs + mmttlv\mmt\descriptors @@ -244,15 +247,12 @@ dantto4k\xml - + dantto4k\subtitle dantto4k\subtitle - - dantto4k\subtitle - dantto4k @@ -304,12 +304,27 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + mmttlv\mmt\structs + @@ -564,9 +579,12 @@ mmttlv\tlv\structs - - mmttlv\tlv\structs - + + mmttlv\tlv\structs + + + mmttlv\tlv\structs + mmttlv\mmt\descriptors @@ -594,10 +612,7 @@ dantto4k\xml - - dantto4k\subtitle - - + dantto4k\subtitle @@ -672,12 +687,39 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + mmttlv\mmt\structs + @@ -734,5 +776,8 @@ {5dccb4d7-560f-4244-9d84-334de3cf19f2} + + {84aaa3a8-9e4a-4085-9dc5-db67d16195cc} + diff --git a/msvc/dantto4k_dll.vcxproj b/msvc/dantto4k_dll.vcxproj index 1a75873..4b14abe 100644 --- a/msvc/dantto4k_dll.vcxproj +++ b/msvc/dantto4k_dll.vcxproj @@ -29,26 +29,26 @@ DynamicLibrary true - v143 + v145 Unicode DynamicLibrary false - v143 + v145 true Unicode DynamicLibrary true - v143 + v145 MultiByte DynamicLibrary false - v143 + v145 true MultiByte @@ -66,17 +66,17 @@ - - - - - - - - - - - + + + + + + + + + + + BonDriver_dantto4k $(Platform)\$(Configuration)\dantto4k_dll\ @@ -125,40 +125,42 @@ Level3 true - _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;_DEBUG;DANTTO4KDLL_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) - true - stdcpp20 - MultiThreadedDebug - /FS %(AdditionalOptions) - + _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;_DEBUG;DANTTO4KDLL_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreadedDebugDLL + /FS %(AdditionalOptions) + $(ProjectDir)../src;%(AdditionalIncludeDirectories) + Windows true false - %(AdditionalDependencies) - - + %(AdditionalDependencies) + + Level3 true true true - _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;NDEBUG;DANTTO4KDLL_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) - true - stdcpp20 - MultiThreaded - /FS %(AdditionalOptions) - + _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;NDEBUG;DANTTO4KDLL_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreaded + /FS %(AdditionalOptions) + $(ProjectDir)../src;%(AdditionalIncludeDirectories) + Windows true true true false - %(AdditionalDependencies) - - + %(AdditionalDependencies) + + @@ -175,13 +177,13 @@ - + - - - - + + + + @@ -204,7 +206,8 @@ - + + @@ -213,7 +216,6 @@ - @@ -262,7 +264,12 @@ - + + + + + + @@ -280,13 +287,13 @@ - + - - - - + + + + @@ -316,7 +323,8 @@ - + + @@ -326,7 +334,6 @@ - @@ -384,9 +391,18 @@ - + + + + + + + + + + diff --git a/msvc/dantto4k_dll.vcxproj.filters b/msvc/dantto4k_dll.vcxproj.filters index 6cae4f5..9cf4f00 100644 --- a/msvc/dantto4k_dll.vcxproj.filters +++ b/msvc/dantto4k_dll.vcxproj.filters @@ -220,9 +220,12 @@ mmttlv\tlv\structs - - mmttlv\tlv\structs - + + mmttlv\tlv\structs + + + mmttlv\tlv\structs + mmttlv\mmt\descriptors @@ -244,15 +247,12 @@ dantto4k\xml - - dantto4k\subtitle - - - dantto4k\subtitle - - - dantto4k\subtitle - + + dantto4k\subtitle + + + mmttlv\mmt\structs + dantto4k @@ -304,12 +304,27 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\subtitle + @@ -564,9 +579,12 @@ mmttlv\tlv\structs - - mmttlv\tlv\structs - + + mmttlv\tlv\structs + + + mmttlv\tlv\structs + mmttlv\mmt\descriptors @@ -594,15 +612,12 @@ dantto4k\xml - - dantto4k\subtitle - - - dantto4k\subtitle - - - dantto4k\subtitle - + + dantto4k\subtitle + + + mmttlv\mmt\structs + dantto4k\subtitle @@ -672,12 +687,39 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\subtitle + @@ -734,5 +776,8 @@ {5dccb4d7-560f-4244-9d84-334de3cf19f2} + + {e31c2a9a-285e-49f0-bf5f-da6ccd870531} + diff --git a/src/additionalAribSubtitleInfo.cpp b/src/additionalAribSubtitleInfo.cpp new file mode 100644 index 0000000..a806ab1 --- /dev/null +++ b/src/additionalAribSubtitleInfo.cpp @@ -0,0 +1,49 @@ +#include "additionalAribSubtitleInfo.h" + +namespace MmtTlv { + +bool AdditionalAribSubtitleInfo::unpack(Common::ReadStream& stream) { + try { + subtitleTag = stream.get8U(); + + uint8_t uint8 = stream.get8U(); + subtitle_info_version = (uint8 & 0b11110000) >> 4; + start_mpu_sequence_number_flag = (uint8 & 0b00001000) >> 3; + + stream.read(languageCode.data(), languageCode.size()); + + uint8 = stream.get8U(); + type = (uint8 & 0b11000000) >> 6; + subtitleFormat = (uint8 & 0b00111100) >> 2; + opm = uint8 & 0b00000011; + + uint8 = stream.get8U(); + tmd = (uint8 & 0b11110000) >> 4; + dmf = uint8 & 0b00001111; + + uint8 = stream.get8U(); + resolution = static_cast((uint8 & 0b11110000) >> 4); + compressionType = uint8 & 0b00001111; + + startMpuSequenceNumber = 0; + if (start_mpu_sequence_number_flag) { + startMpuSequenceNumber = stream.getBe32U(); + } + + referenceStartTime = {}; + referenceStartTimeLeapIndicator = 0; + if (tmd == 0b0010) { + if (!referenceStartTime.unpack(stream)) { + return false; + } + referenceStartTimeLeapIndicator = stream.get8U() >> 6; + } + } + catch (const std::out_of_range&) { + return false; + } + + return true; +} + +} diff --git a/src/additionalAribSubtitleInfo.h b/src/additionalAribSubtitleInfo.h new file mode 100644 index 0000000..d57561e --- /dev/null +++ b/src/additionalAribSubtitleInfo.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include "stream.h" +#include "ntpTimestamp.h" + +namespace MmtTlv { + +enum class SubtitleResolution : uint8_t { + Resolution2K = 0, // 1920 x 1080 + Resolution4K = 1, // 3840 x 2160 + Resolution8K = 2, // 7680 x 4320 +}; + +class AdditionalAribSubtitleInfo { +public: + bool unpack(Common::ReadStream& stream); + + uint8_t subtitleTag{}; + uint8_t subtitle_info_version{}; + bool start_mpu_sequence_number_flag{}; + std::array languageCode; + uint8_t type{}; + uint8_t subtitleFormat{}; + uint8_t opm{}; + uint8_t tmd{}; + uint8_t dmf{}; + SubtitleResolution resolution{SubtitleResolution::Resolution4K}; + uint8_t compressionType{}; + uint32_t startMpuSequenceNumber{}; + NtpTimestamp referenceStartTime; + uint8_t referenceStartTimeLeapIndicator{}; + +}; + +} diff --git a/src/aribCharsetEncoder.cpp b/src/aribCharsetEncoder.cpp index 7b475a8..30742c7 100644 --- a/src/aribCharsetEncoder.cpp +++ b/src/aribCharsetEncoder.cpp @@ -9,7 +9,11 @@ #include #include -namespace arib::charset::detail { +namespace arib { + +namespace charset { + +namespace detail { struct CharLookupResult { const Charset* charset; @@ -73,7 +77,7 @@ const CharLookupTable& charLookupTable() { return table; } -std::optional findChar(char32_t c, CharsetCode candidate1, CharsetCode candidate2) { +std::optional findChar(char32_t c, CharsetCode candidate1, CharsetCode candidate2, EncodeMode encodeMode) { const auto& table = charLookupTable(); auto it = table.find(c); if (it == table.end()) { @@ -82,52 +86,70 @@ std::optional findChar(char32_t c, CharsetCode candidate1, Cha const auto& results = it->second; if (results.empty()) { - return std::nullopt; + return {}; } - if (results.size() == 1) { - return results[0]; + if (encodeMode == EncodeMode::Caption) { + const auto additionalSymbol = std::find_if(results.begin(), results.end(), [](const auto& result) { + return result.charset->code == CharsetCode::AdditionalSymbols; + }); + if (additionalSymbol != results.end()) { + return *additionalSymbol; + } } if (candidate1 != CharsetCode::None && - candidate1 != CharsetCode::JISKanjiPlane1 && - candidate1 != CharsetCode::JISKanjiPlane2) { + (encodeMode == EncodeMode::Caption || + (candidate1 != CharsetCode::JISKanjiPlane1 && + candidate1 != CharsetCode::JISKanjiPlane2))) { for (const auto& res : results) { - if (res.charset->code == candidate1) return res; + if (res.charset->code == candidate1) { + return res; + } } } if (candidate2 != CharsetCode::None && - candidate2 != CharsetCode::JISKanjiPlane1 && - candidate2 != CharsetCode::JISKanjiPlane2) { + (encodeMode == EncodeMode::Caption || + (candidate2 != CharsetCode::JISKanjiPlane1 && + candidate2 != CharsetCode::JISKanjiPlane2))) { for (const auto& res : results) { - if (res.charset->code == candidate2) return res; + if (res.charset->code == candidate2) { + return res; + } } } - return results[0]; + return results.front(); } -std::optional findCharsetBy2Char(char32_t c1, char32_t c2, CharsetCode candidate1, CharsetCode candidate2) { +std::optional findCharsetBy2Char(char32_t c1, char32_t c2, CharsetCode candidate1, CharsetCode candidate2, EncodeMode encodeMode) { const auto& table = charLookupTable(); auto it1 = table.find(c1); - if (it1 == table.end()) return std::nullopt; + if (it1 == table.end()) { + return {}; + } const auto& results1 = it1->second; auto it2 = table.find(c2); - if (it2 == table.end()) return std::nullopt; + if (it2 == table.end()) { + return {}; + } const auto& results2 = it2->second; auto hasCharset = [](const std::vector& list, const Charset* target) { for (const auto& res : list) { - if (res.charset == target) return true; + if (res.charset == target) { + return true; + } } return false; }; if (candidate1 != CharsetCode::None && - candidate1 != CharsetCode::JISKanjiPlane1 && - candidate1 != CharsetCode::JISKanjiPlane2) { + (encodeMode == EncodeMode::Caption || + (candidate1 != CharsetCode::JISKanjiPlane1 && + candidate1 != CharsetCode::JISKanjiPlane2))) { const Charset* cs = findCharset(candidate1); if (cs != nullptr) { bool found1 = false; @@ -147,8 +169,9 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha } if (candidate2 != CharsetCode::None && - candidate2 != CharsetCode::JISKanjiPlane1 && - candidate2 != CharsetCode::JISKanjiPlane2) { + (encodeMode == EncodeMode::Caption || + (candidate2 != CharsetCode::JISKanjiPlane1 && + candidate2 != CharsetCode::JISKanjiPlane2))) { const Charset* cs = findCharset(candidate2); if (cs != nullptr) { bool found1 = false; @@ -235,8 +258,8 @@ class Encoder { }; const EncodeMode mode; - State state; - + State state; + bool isFullwidth(char32_t ch) { for (const char32_t* p = fullwidthChars; *p != 0; ++p) { if (*p == ch) { @@ -292,7 +315,7 @@ class Encoder { CharsetCode glCode = state.graphic[state.gl]; CharsetCode grCode = state.graphic[state.gr]; - auto find = findChar(c, glCode, grCode); + auto find = findChar(c, glCode, grCode, mode); if (!find) { ++pos; @@ -305,7 +328,7 @@ class Encoder { // If the character is neither Hiragana nor Katakana nor AdditionalSymbol if (!(c >= 0x3041 && c <= 0x3093) && !(c >= 0x30A1 && c <= 0x30F6) && find->charset->code != CharsetCode::AdditionalSymbols) { // Check if the current and next characters belong to the same charset - auto find2 = findCharsetBy2Char(c, state.u32[pos + 1], glCode, grCode); + auto find2 = findCharsetBy2Char(c, state.u32[pos + 1], glCode, grCode, mode); if (find2) { find = find2; } @@ -345,8 +368,15 @@ class Encoder { } } - void selectCharset(const Charset* charset, size_t pos) { - uint8_t graphicIndex = findGraphicByCharsetCode(charset->code); + void selectCharset(const Charset* charset, size_t pos) { + if (mode == EncodeMode::Caption && charset->code == CharsetCode::AdditionalSymbols) { + charset = findCharset(CharsetCode::JISKanjiPlane1); + if (charset == nullptr) { + return; + } + } + + uint8_t graphicIndex = findGraphicByCharsetCode(charset->code); // Check single shift if (charset->code == CharsetCode::Hiragana || charset->code == CharsetCode::Katakana) { @@ -444,17 +474,22 @@ class Encoder { return; } + CharsetCode newCharsetCode = charsetCode; + if (mode == EncodeMode::Caption && charsetCode == CharsetCode::JISKanjiPlane1) { + newCharsetCode = CharsetCode::Kanji; + } + state.output.push_back(ESC); if (charset->is2Byte == false) { state.output.push_back(0x28 + graphicIndex); - state.output.push_back(static_cast(charsetCode)); + state.output.push_back(static_cast(newCharsetCode)); } else { state.output.push_back(0x24); if (graphicIndex >= 1 && graphicIndex <= 3) { state.output.push_back(0x29 + graphicIndex); } - state.output.push_back(static_cast(charsetCode)); + state.output.push_back(static_cast(newCharsetCode)); } state.graphic[graphicIndex] = static_cast(charsetCode); @@ -519,7 +554,7 @@ class Encoder { size_t getCharsetRunLength(CharsetCode charsetCode, size_t pos) { size_t count = 0; for (size_t i = pos; i < state.u32.size(); i++) { - auto result = findChar(state.u32[i], charsetCode, CharsetCode::None); + auto result = findChar(state.u32[i], charsetCode, CharsetCode::None, mode); if (!result) { return count; } @@ -536,7 +571,7 @@ class Encoder { const Charset* getNextCharset(CharsetCode currentCharsetCode, size_t pos) { for (size_t i = pos; i < state.u32.size(); i++) { - auto result = findChar(state.u32[i], currentCharsetCode, CharsetCode::None); + auto result = findChar(state.u32[i], currentCharsetCode, CharsetCode::None, mode); if (!result) { return nullptr; } @@ -583,9 +618,7 @@ class Encoder { }; -} // namespace arib::charset::detail - -namespace arib::charset { +} // namespace detail std::string encode(std::string_view input, EncodeMode mode) { detail::Encoder encoder(mode); @@ -598,4 +631,6 @@ std::string encode(std::u8string_view input, EncodeMode mode) { return encoder.encode(input); } -} // namespace arib::charset +} // namespace charset + +} // namespace arib diff --git a/src/b24Color.cpp b/src/b24Color.cpp index e8d786a..381d498 100644 --- a/src/b24Color.cpp +++ b/src/b24Color.cpp @@ -157,10 +157,10 @@ std::pair findClosestColor(const ColorRGBA& color) { uint32_t distance = 0xFFFFFFFF; for (uint8_t p = 0; p < 8; p++) { for (uint8_t i = 0; i < 16; i++) { - uint8_t r = color.r - kB24ColorCLUT[p][i].r; - uint8_t g = color.g - kB24ColorCLUT[p][i].g; - uint8_t b = color.b - kB24ColorCLUT[p][i].b; - uint8_t a = color.a - kB24ColorCLUT[p][i].a; + int32_t r = color.r - kB24ColorCLUT[p][i].r; + int32_t g = color.g - kB24ColorCLUT[p][i].g; + int32_t b = color.b - kB24ColorCLUT[p][i].b; + int32_t a = color.a - kB24ColorCLUT[p][i].a; uint32_t d = r * r + g * g + b * b + a * a; if (d < distance) { distance = d; diff --git a/src/b24ControlSet.h b/src/b24ControlSet.h index c310046..607c66a 100644 --- a/src/b24ControlSet.h +++ b/src/b24ControlSet.h @@ -54,6 +54,7 @@ constexpr uint8_t SPL = 0x99; constexpr uint8_t STL = 0x9A; constexpr uint8_t CSI = 0x9B; constexpr uint8_t TIME = 0x9D; -constexpr uint8_t ACPS = 0x61; - -} \ No newline at end of file +constexpr uint8_t ACPS = 0x61; +constexpr uint8_t MDF = 0x64; + +} diff --git a/src/b24SubtitleConverter.cpp b/src/b24SubtitleConverter.cpp new file mode 100644 index 0000000..b1d8a21 --- /dev/null +++ b/src/b24SubtitleConverter.cpp @@ -0,0 +1,61 @@ +#include "b24SubtitleConverter.h" +#include "aribTextEncoder.h" +#include "b24Color.h" +#include "b24ControlSet.h" +#include "ttml/parser.h" +#include "ttml/resolver.h" +#include "ttml/b24_converter.h" +#include +#include + +bool B24SubtitleConverter::convert(const std::string& input, std::list& output, B24::PESData::PESType pesType, MmtTlv::SubtitleResolution resolution) { + const auto sync_mode = pesType == B24::PESData::PESType::Synchronized + ? arib::ttml::SyncMode::Sync + : arib::ttml::SyncMode::Async; + auto parse_result = arib::ttml::parse(input, sync_mode); + if (parse_result.error) { +#ifdef _DEBUG + std::cerr << "[TTML] " << parse_result.error->message << std::endl; +#endif + return false; + } + auto resolve_result = arib::ttml::resolve(*parse_result.document, sync_mode); + if (resolve_result.error) { +#ifdef _DEBUG + std::cerr << "[TTML] " << resolve_result.error->message << std::endl; +#endif + return false; + } + auto convert_result = arib::ttml::convert_to_b24(*resolve_result.document, sync_mode, resolution); + if (convert_result.error) { +#ifdef _DEBUG + std::cerr << "[TTML] " << convert_result.error->message << std::endl; +#endif + return false; + } + + for (auto& converted : convert_result.outputs) { + B24::CaptionStatementData captionStatementData; + captionStatementData.dataUnits.push_back({ {B24ControlSet::CS} }); + if (converted.data.size()) { + captionStatementData.dataUnits.push_back(std::move(converted.data)); + } + + B24::DataGroup dataGroup; + dataGroup.setGroupData(captionStatementData); + + std::vector packedPesData; + B24::PESData pesData(dataGroup); + pesData.SetPESType(pesType); + pesData.pack(packedPesData); + + output.push_back({ + packedPesData, + converted.begin + ? std::optional{static_cast(converted.begin->count())} + : std::nullopt, + }); + } + + return true; +} diff --git a/src/b24SubtitleConvertor.h b/src/b24SubtitleConverter.h similarity index 95% rename from src/b24SubtitleConvertor.h rename to src/b24SubtitleConverter.h index 826390a..ba8690d 100644 --- a/src/b24SubtitleConvertor.h +++ b/src/b24SubtitleConverter.h @@ -6,8 +6,9 @@ #include #include #include -#include -#include "ttml.h" +#include +#include +#include "additionalAribSubtitleInfo.h" namespace B24 { enum class DataUnitParameter : uint8_t { @@ -99,7 +100,7 @@ namespace B24 { uint8_t languageTag{0}; uint8_t dmf{0}; uint8_t dc{0}; - std::string languageCode; + std::array languageCode; uint8_t format{0}; uint8_t tcs{0}; uint8_t rollupMode{0}; @@ -302,19 +303,12 @@ class B24SubtitleOutput { B24SubtitleOutput(std::vector pesData, std::optional begin) : pesData(pesData), begin(begin) {} - uint64_t calcPts(uint64_t programStartTime) const { - if (!begin) { - return 0; - } - return (programStartTime * 1000 + *begin) * 90; - } - std::vector pesData; std::optional begin; }; -class B24SubtitleConvertor { -public: - static bool convert(const std::string& input, std::list& output); - -}; \ No newline at end of file +class B24SubtitleConverter { +public: + static bool convert(const std::string& input, std::list& output, B24::PESData::PESType pesType, MmtTlv::SubtitleResolution resolution); + +}; diff --git a/src/b24SubtitleConvertor.cpp b/src/b24SubtitleConvertor.cpp deleted file mode 100644 index dac4960..0000000 --- a/src/b24SubtitleConvertor.cpp +++ /dev/null @@ -1,269 +0,0 @@ -#include "b24SubtitleConvertor.h" -#include "aribTextEncoder.h" -#include "b24Color.h" -#include "b24ControlSet.h" -#include - -namespace { - -std::vector splitByNull(const std::string& data) { - std::vector tokens; - std::string current; - for (auto byte : data) { - if (byte == 0) { - tokens.push_back(current); - current.clear(); - } - else { - current.push_back(static_cast(byte)); - } - } - if (!current.empty()) { - tokens.push_back(current); - } - return tokens; -} - -void appendNumber(std::vector& output, int n) { - if (n == 0) { - output.push_back(0x30); - return; - } - std::vector temp; - while (n > 0) { - temp.push_back(static_cast((n % 10) + 0x30)); - n /= 10; - } - std::reverse(temp.begin(), temp.end()); - output.insert(output.end(), temp.begin(), temp.end()); -} - -} - -bool B24SubtitleConvertor::convert(const std::string& input, std::list& output) { - const TTML ttml = TTMLPaser::parse(input); - - uint8_t lastTextColorPalette = 0; - uint8_t lastTextColorIndex = 7; - uint8_t lastBackgroundColorPalette = 0; - uint8_t lastBackgroundColorIndex = 8; - uint8_t characterSize = B24ControlSet::NSZ; - double fontHeight = 0; - - for (const auto& div : ttml.divTags) { - B24::CaptionStatementData captionStatementData; - - std::string text; - for (const auto& p : div.pTags) { - for (const auto& span : p.spanTags) { - text.append(span.text); - text.push_back('\0'); - } - } - - auto encoded = arib::text::encode(text, arib::charset::EncodeMode::Caption); - auto encodedSplit = splitByNull(encoded); - int encodedSplitIndex = 0; - - std::vector unitDataByte; - unitDataByte.push_back(B24ControlSet::CS); - - // Set writing format 960 x 540 - unitDataByte.push_back(B24ControlSet::CSI); - unitDataByte.push_back(0x37); - unitDataByte.push_back(B24ControlSet::SP); - unitDataByte.push_back(B24ControlSet::SWF); - - for (const auto& p : div.pTags) { - if (p.spanTags.empty()) { - continue; - } - - // Set display format - if (p.region.extent.has_value()) { - unitDataByte.push_back(B24ControlSet::CSI); - appendNumber(unitDataByte, static_cast(p.region.extent->first.getValue().value * 960 / 3840)); - unitDataByte.push_back(0x3B); - appendNumber(unitDataByte, static_cast(p.region.extent->second.getValue().value * 540 / 2160)); - unitDataByte.push_back(B24ControlSet::SP); - unitDataByte.push_back(B24ControlSet::SDF); - } - - // Set character size - const auto firstSpan = p.spanTags.begin(); - if (firstSpan != p.spanTags.end() && firstSpan->style.fontSize) { - TTMLCssValueLength first = firstSpan->style.fontSize->first.getValue(); - TTMLCssValueLength second = firstSpan->style.fontSize->second.getValue(); - - if (first.value == 144 && second.value == 144) { - if (characterSize != B24ControlSet::NSZ) { - unitDataByte.push_back(B24ControlSet::NSZ); - characterSize = B24ControlSet::NSZ; - } - fontHeight = 240; - } - else if (first.value == 72 && second.value == 144) { - if (characterSize != B24ControlSet::MSZ) { - unitDataByte.push_back(B24ControlSet::MSZ); - characterSize = B24ControlSet::MSZ; - } - fontHeight = 240; - } - else if (first.value == 72 && second.value == 72) { - if (characterSize != B24ControlSet::SSZ) { - unitDataByte.push_back(B24ControlSet::SSZ); - characterSize = B24ControlSet::SSZ; - } - fontHeight = 120; - } - } - - // Set position - if (p.region.origin.has_value()) { - double offsetY = 0; - if (p.spanTags.begin()->style.lineHeight.has_value() && - p.spanTags.begin()->style.fontSize) { - double lineHeight = p.spanTags.begin()->style.lineHeight->getValue().value; - if (p.region.extent.has_value()) { - offsetY = (lineHeight - fontHeight) / 2; - } - } - - unitDataByte.push_back(B24ControlSet::CSI); - appendNumber(unitDataByte, static_cast(p.region.origin->first.getValue().value * 960 / 3840)); - unitDataByte.push_back(0x3B); - appendNumber(unitDataByte, static_cast((p.region.origin->second.getValue().value + offsetY) * 540 / 2160)); - unitDataByte.push_back(B24ControlSet::SP); - unitDataByte.push_back(B24ControlSet::SDP); - } - - // Set active position to 0 x 0 - unitDataByte.push_back(B24ControlSet::APS); - unitDataByte.push_back(0x40); - unitDataByte.push_back(0x40); - - TTMLStyle oldStyle; - for (const auto& span : p.spanTags) { - // Set character size - if (span.style.fontSize) { - TTMLCssValueLength first = span.style.fontSize->first.getValue(); - TTMLCssValueLength second = span.style.fontSize->second.getValue(); - - if (first.value == 144 && second.value == 144) { - if (characterSize != B24ControlSet::NSZ) { - unitDataByte.push_back(B24ControlSet::NSZ); - characterSize = B24ControlSet::NSZ; - } - } - else if (first.value == 72 && second.value == 144) { - if (characterSize != B24ControlSet::MSZ) { - unitDataByte.push_back(B24ControlSet::MSZ); - characterSize = B24ControlSet::MSZ; - } - } - else if (first.value == 72 && second.value == 72) { - if (characterSize != B24ControlSet::SSZ) { - unitDataByte.push_back(B24ControlSet::SSZ); - characterSize = B24ControlSet::SSZ; - } - } - } - - // Set background color - if (span.style.backgroundColor.has_value()) { - TTMLCssValueColor color = span.style.backgroundColor->getValue(); - auto closetColor = findClosestColor(ColorRGBA{ color.r, color.g, color.b, color.a }); - - if (lastBackgroundColorPalette != closetColor.first || lastBackgroundColorIndex != closetColor.second) { - unitDataByte.push_back(B24ControlSet::COL); - unitDataByte.push_back(0x20); - unitDataByte.push_back(0x40 | closetColor.first); - unitDataByte.push_back(B24ControlSet::COL); - unitDataByte.push_back(0x50 | closetColor.second); - - lastBackgroundColorPalette = closetColor.first; - lastBackgroundColorIndex = closetColor.second; - } - } - - // Set text color - if (span.style.color.has_value()) { - TTMLCssValueColor color = span.style.color->getValue(); - auto closetColor = findClosestColor(ColorRGBA{ color.r, color.g, color.b, color.a }); - - if (lastTextColorPalette != closetColor.first || lastTextColorIndex != closetColor.second) { - if (closetColor.first == 0 && closetColor.second >= 0 && closetColor.second <= 7) { - unitDataByte.push_back(B24ControlSet::COL); - unitDataByte.push_back(0x20); - unitDataByte.push_back(0x40 | 0); - unitDataByte.push_back(B24ControlSet::BKF + closetColor.second); - } - else { - unitDataByte.push_back(B24ControlSet::COL); - unitDataByte.push_back(0x20); - unitDataByte.push_back(0x40 | closetColor.first); - unitDataByte.push_back(B24ControlSet::COL); - unitDataByte.push_back(0x40 | closetColor.second); - } - - lastTextColorPalette = closetColor.first; - lastTextColorIndex = closetColor.second; - } - } - - - unitDataByte.insert(unitDataByte.end(), encodedSplit[encodedSplitIndex].begin(), encodedSplit[encodedSplitIndex].end()); - encodedSplitIndex++; - } - } - - captionStatementData.dataUnits.push_back({ unitDataByte }); - - if (div.begin && div.end) { - std::vector unitDataByte; - - uint64_t duration = (*div.end - *div.begin) / 100; - while (duration > 0) { - uint8_t value = static_cast(std::min(duration, static_cast(0x3F))); - unitDataByte.push_back(B24ControlSet::TIME); - unitDataByte.push_back(0x20); - unitDataByte.push_back(0x40 | value); - unitDataByte.push_back(0x0C); - duration -= value; - } - - captionStatementData.dataUnits.push_back({ unitDataByte }); - } - - B24::DataGroup dataGroup; - dataGroup.setGroupData(captionStatementData); - - std::vector packedPesData; - B24::PESData pesData(dataGroup); - pesData.SetPESType(B24::PESData::PESType::Synchronized); - pesData.pack(packedPesData); - - output.push_back({ packedPesData , div.begin }); - } - - if (output.size() == 0) { - B24::CaptionStatementData captionStatementData; - { - // clear - std::vector unitDataByte; - unitDataByte.push_back(B24ControlSet::CS); - captionStatementData.dataUnits.push_back({ unitDataByte }); - } - B24::DataGroup dataGroup; - dataGroup.setGroupData(captionStatementData); - - std::vector packedPesData; - B24::PESData pesData(dataGroup); - pesData.SetPESType(B24::PESData::PESType::Synchronized); - pesData.pack(packedPesData); - - output.push_back({ packedPesData , 0 }); - } - - return true; -} diff --git a/src/dantto4k.cpp b/src/dantto4k.cpp index 676385d..90535dd 100644 --- a/src/dantto4k.cpp +++ b/src/dantto4k.cpp @@ -7,16 +7,16 @@ #include "casProxyClient.h" #include "acasHandler.h" #include "smartCard.h" -#include "bufferedOutput.h" -#include "progressReporter.h" - -#ifdef WIN32 -#include -#include -#include -#endif - -namespace { +#include "bufferedOutput.h" +#include "progressReporter.h" + +#ifdef WIN32 +#include +#include +#include +#endif + +namespace { struct Args { std::string input; @@ -165,25 +165,25 @@ int main(int argc, char* argv[]) { bool useStdin = (args.input == "-"); bool useStdout = (args.output == "-"); - if (args.listSmartCardReader) { - printReaderList(args); - return 0; - } - -#ifdef WIN32 - if (useStdin && _setmode(_fileno(stdin), _O_BINARY) == -1) { - std::cerr << "Unable to set stdin to binary mode" << std::endl; - return 1; - } - if (useStdout && _setmode(_fileno(stdout), _O_BINARY) == -1) { - std::cerr << "Unable to set stdout to binary mode" << std::endl; - return 1; - } -#endif - - std::istream* inputStream; - std::unique_ptr inputFs; - if (useStdin) { + if (args.listSmartCardReader) { + printReaderList(args); + return 0; + } + +#ifdef WIN32 + if (useStdin && _setmode(_fileno(stdin), _O_BINARY) == -1) { + std::cerr << "Unable to set stdin to binary mode" << std::endl; + return 1; + } + if (useStdout && _setmode(_fileno(stdout), _O_BINARY) == -1) { + std::cerr << "Unable to set stdout to binary mode" << std::endl; + return 1; + } +#endif + + std::istream* inputStream; + std::unique_ptr inputFs; + if (useStdin) { inputStream = &std::cin; } else { @@ -261,30 +261,30 @@ int main(int argc, char* argv[]) { } } - std::vector inputBuffer; - std::vector readBuffer(chunkSize); - inputBuffer.reserve(chunkSize * 2); - bool inputEnded = false; - while (true) { - if (!inputEnded && inputBuffer.size() < chunkSize) { - inputStream->read(reinterpret_cast(readBuffer.data()), static_cast(readBuffer.size())); - std::streamsize bytesRead = inputStream->gcount(); - if (bytesRead > 0) { - auto readSize = static_cast(bytesRead); - inputBuffer.insert(inputBuffer.end(), readBuffer.data(), readBuffer.data() + readSize); - } - if (bytesRead == 0 && inputStream->eof()) { - inputEnded = true; - } - } - - if (inputBuffer.empty() && inputEnded) { - break; - } - - MmtTlv::Common::ReadStream stream(inputBuffer); - while (!stream.isEof()) { - MmtTlv::DemuxStatus status = demuxer.demux(stream); + std::vector inputBuffer; + std::vector readBuffer(chunkSize); + inputBuffer.reserve(chunkSize * 2); + bool inputEnded = false; + while (true) { + if (!inputEnded && inputBuffer.size() < chunkSize) { + inputStream->read(reinterpret_cast(readBuffer.data()), static_cast(readBuffer.size())); + std::streamsize bytesRead = inputStream->gcount(); + if (bytesRead > 0) { + auto readSize = static_cast(bytesRead); + inputBuffer.insert(inputBuffer.end(), readBuffer.data(), readBuffer.data() + readSize); + } + if (bytesRead == 0 && inputStream->eof()) { + inputEnded = true; + } + } + + if (inputBuffer.empty() && inputEnded) { + break; + } + + MmtTlv::Common::ReadStream stream(inputBuffer); + while (!stream.isEof()) { + MmtTlv::DemuxStatus status = demuxer.demux(stream); if (status == MmtTlv::DemuxStatus::NotEnoughBuffer) { break; @@ -292,14 +292,14 @@ int main(int argc, char* argv[]) { } auto consumed = inputBuffer.size() - stream.leftBytes(); - if (consumed > 0) { - progressReporter.update(consumed); - } - inputBuffer.erase(inputBuffer.begin(), inputBuffer.begin() + consumed); - if (inputEnded && consumed == 0) { - break; - } - } + if (consumed > 0) { + progressReporter.update(consumed); + } + inputBuffer.erase(inputBuffer.begin(), inputBuffer.begin() + consumed); + if (inputEnded && consumed == 0) { + break; + } + } progressReporter.finish(); if (!args.noStats) { diff --git a/src/mhDataComponentDescriptor.cpp b/src/mhDataComponentDescriptor.cpp index 2deddc1..f25e1ef 100644 --- a/src/mhDataComponentDescriptor.cpp +++ b/src/mhDataComponentDescriptor.cpp @@ -2,8 +2,7 @@ namespace MmtTlv { -bool MhDataComponentDescriptor::unpack(Common::ReadStream& stream) -{ +bool MhDataComponentDescriptor::unpack(Common::ReadStream& stream) { try { if (!MmtDescriptorTemplate::unpack(stream)) { return false; diff --git a/src/mmtStream.cpp b/src/mmtStream.cpp index cd65386..b7d6ea0 100644 --- a/src/mmtStream.cpp +++ b/src/mmtStream.cpp @@ -4,9 +4,9 @@ #include "timebase.h" #include -namespace MmtTlv { - -std::pair MmtStream::getNextPtsDts() { +namespace MmtTlv { + +std::pair MmtStream::getNextPtsDts() { const auto timestamp = getCurrentTimestamp(); int64_t ptime = av_rescale(timestamp.first.mpuPresentationTime, timeBase.den, @@ -66,4 +66,4 @@ uint32_t MmtStream::getSamplingRate() const { return mhAudioComponentDescriptor->getConvertedSamplingRate(); } -} \ No newline at end of file +} diff --git a/src/mmtStream.h b/src/mmtStream.h index bbcd569..93f1fe5 100644 --- a/src/mmtStream.h +++ b/src/mmtStream.h @@ -1,12 +1,13 @@ #pragma once -#include -#include -#include +#include +#include +#include #include "mpuExtendedTimestampDescriptor.h" #include "mpuTimestampDescriptor.h" #include "videoComponentDescriptor.h" #include "mhAudioComponentDescriptor.h" #include "mpuProcessorBase.h" +#include "additionalAribSubtitleInfo.h" namespace MmtTlv { @@ -46,10 +47,22 @@ class MmtStream final { const std::vector& getMpuExtendedTimestamps() const { return mpuExtendedTimestamps; } const TimeBase& getTimeBase() const { return timeBase; } -private: - friend class MmtTlvDemuxer; + const std::optional& additionalAribSubtitleInfo() const noexcept { + return subtitleInfo; + } + + bool isClosedCaption() const { + return assetType == AssetType::stpp && componentTag >= 0x30 && componentTag <= 0x37; + } + + bool isSuperimposition() const { + return assetType == AssetType::stpp && componentTag >= 0x38 && componentTag <= 0x3F; + } - std::pair getCurrentTimestamp() const; +private: + friend class MmtTlvDemuxer; + + std::pair getCurrentTimestamp() const; uint16_t packetId; uint32_t assetType{0}; @@ -63,6 +76,7 @@ class MmtStream final { std::optional mhAudioComponentDescriptor; std::vector mpuTimestamps; std::vector mpuExtendedTimestamps; + std::optional subtitleInfo; TimeBase timeBase{0, 0}; }; diff --git a/src/mmtTlvDemuxer.cpp b/src/mmtTlvDemuxer.cpp index eaad600..5b166c6 100644 --- a/src/mmtTlvDemuxer.cpp +++ b/src/mmtTlvDemuxer.cpp @@ -24,14 +24,15 @@ #include "mhStreamIdentificationDescriptor.h" #include "videoComponentDescriptor.h" #include "mhAudioComponentDescriptor.h" +#include "mhDataComponentDescriptor.h" #include "ipv6.h" #include "ntp.h" #include "dataTransmissionMessage.h" #include "caMessage.h" #include "damt.h" -#include #include "ddmt.h" #include "dcct.h" +#include namespace MmtTlv { @@ -502,6 +503,10 @@ void MmtTlvDemuxer::processMmtPackageTable(const Mpt& mpt) { continue; } + if (asset.assetType == AssetType::stpp) { + mmtStream->subtitleInfo.reset(); + } + for (const auto& descriptor : asset.descriptors.list) { switch (descriptor->getDescriptorTag()) { case MpuTimestampDescriptor::kDescriptorTag: @@ -538,6 +543,17 @@ void MmtTlvDemuxer::processMmtPackageTable(const Mpt& mpt) { statistics.getMmtStat(mmtStream->packetId)->audioSamplingRate = mmtDescriptor->samplingRate; break; } + case MhDataComponentDescriptor::kDescriptorTag: + { + const auto* mmtDescriptor = static_cast(descriptor.get()); + if (asset.assetType == AssetType::stpp && mmtDescriptor->dataComponentId == 0x0020) { + AdditionalAribSubtitleInfo subtitleInfo; + Common::ReadStream stream(mmtDescriptor->additionalDataComponentInfo); + subtitleInfo.unpack(stream); + mmtStream->subtitleInfo = std::move(subtitleInfo); + } + break; + } } } } @@ -809,7 +825,7 @@ void MmtTlvDemuxer::processMfuData(Common::ReadStream& stream) { std::vector data(stream.leftBytes()); stream.read(data.data(), stream.leftBytes()); - const auto ret = mmtStream->mpuProcessor->process(*mmtStream, data, mpu.fragmentationIndicator); + auto ret = mmtStream->mpuProcessor->process(*mmtStream, data, mpu.fragmentationIndicator); if (ret) { const auto& mfuData = ret.value(); auto* mmtStream = getStreamByIdx(mfuData.streamIndex); @@ -870,7 +886,7 @@ void MmtTlvDemuxer::processSignalingMessages(Common::ReadStream& stream) { std::vector message; message.resize(length); - stream.read(message.data(), length); + nstream.read(message.data(), length); if (assembler->assemble(message, signalingMessage.fragmentationIndicator, mmtp.packetSequenceNumber)) { Common::ReadStream messageStream(assembler->data); diff --git a/src/mpuProcessorBase.h b/src/mpuProcessorBase.h index 9b9c751..da40f59 100644 --- a/src/mpuProcessorBase.h +++ b/src/mpuProcessorBase.h @@ -24,8 +24,8 @@ constexpr uint64_t NOPTS_VALUE = 0x8000000000000000; struct MfuData { std::vector data; uint64_t pts{NOPTS_VALUE}; - uint64_t dts{NOPTS_VALUE}; - int streamIndex{}; + uint64_t dts{NOPTS_VALUE}; + int streamIndex{}; bool keyframe{false}; bool isFirstFragment{false}; bool isLastFragment{false}; @@ -50,4 +50,4 @@ class MpuProcessorTemplate : public MpuProcessorBase { }; -} \ No newline at end of file +} diff --git a/src/ntp.cpp b/src/ntp.cpp index a2cc73d..f94e971 100644 --- a/src/ntp.cpp +++ b/src/ntp.cpp @@ -1,7 +1,6 @@ #include "ntp.h" -bool MmtTlv::NTPv4::unpack(Common::ReadStream& stream) -{ +bool MmtTlv::NTPv4::unpack(Common::ReadStream& stream) { try { uint8_t uint8 = stream.get8U(); leap_indicator = uint8 & 0b11000000; @@ -35,17 +34,3 @@ bool MmtTlv::NTPv4::unpack(Common::ReadStream& stream) return true; } - - -bool MmtTlv::NtpTimestamp::unpack(Common::ReadStream& stream) -{ - try { - seconds = stream.getBe32U(); - fraction = stream.getBe32U(); - } - catch (const std::out_of_range&) { - return false; - } - - return true; -} diff --git a/src/ntp.h b/src/ntp.h index d5617f0..178c7b6 100644 --- a/src/ntp.h +++ b/src/ntp.h @@ -1,50 +1,31 @@ -#pragma once -#include "stream.h" -#include "timebase.h" - -namespace MmtTlv { - - class NtpTimestamp { - public: - bool unpack(Common::ReadStream& stream); - int64_t toPcrValue() const { - const uint32_t NTP_1970 = 2208988800U; - int64_t unixTimestamp = static_cast(((seconds - NTP_1970) * 1000.0) + ((fraction / 4294967296.0) * 1000.0)); - - const AVRational ntpTimeBase = { 1, 1000 }; - const AVRational pcrTimeBase = { 1, 27000000 }; - - return av_rescale_q(unixTimestamp, ntpTimeBase, pcrTimeBase); - } - - public: - uint32_t seconds; - uint32_t fraction; - - }; - - class NTPv4 { - public: - bool unpack(Common::ReadStream& stream); - - public: - uint8_t leap_indicator; - uint8_t version_number; - uint8_t mode; - - uint8_t stratum; - uint8_t poll_interval; - uint8_t precision; - - uint32_t root_delay; - uint32_t root_dispersion; - uint32_t reference_id; - - NtpTimestamp reference_timestamp; - NtpTimestamp origin_timestamp; - NtpTimestamp receive_timestamp; - NtpTimestamp transmit_timestamp; - - }; - -} \ No newline at end of file +#pragma once +#include "stream.h" +#include "ntpTimestamp.h" + +namespace MmtTlv { + +class NTPv4 { +public: + bool unpack(Common::ReadStream& stream); + +public: + uint8_t leap_indicator; + uint8_t version_number; + uint8_t mode; + + uint8_t stratum; + uint8_t poll_interval; + uint8_t precision; + + uint32_t root_delay; + uint32_t root_dispersion; + uint32_t reference_id; + + NtpTimestamp reference_timestamp; + NtpTimestamp origin_timestamp; + NtpTimestamp receive_timestamp; + NtpTimestamp transmit_timestamp; + +}; + +} diff --git a/src/ntpTimestamp.cpp b/src/ntpTimestamp.cpp new file mode 100644 index 0000000..340f812 --- /dev/null +++ b/src/ntpTimestamp.cpp @@ -0,0 +1,31 @@ +#include "ntpTimestamp.h" +#include "timebase.h" + +namespace MmtTlv { + +bool NtpTimestamp::unpack(Common::ReadStream& stream) { + try { + seconds = stream.getBe32U(); + fraction = stream.getBe32U(); + } + catch (const std::out_of_range&) { + return false; + } + + return true; +} + +int64_t NtpTimestamp::toPcrValue() const { + constexpr int64_t NTP_1970 = 2208988800LL; + const int64_t unixTimestamp = (static_cast(seconds) - NTP_1970) * 1000 + + av_rescale(fraction, 1000, UINT64_C(1) << 32); + const AVRational ntpTimeBase = { 1, 1000 }; + const AVRational pcrTimeBase = { 1, 27000000 }; + return av_rescale_q(unixTimestamp, ntpTimeBase, pcrTimeBase); +} + +int64_t NtpTimestamp::toPtsValue() const { + return toPcrValue() / 300; +} + +} diff --git a/src/ntpTimestamp.h b/src/ntpTimestamp.h new file mode 100644 index 0000000..2783f4f --- /dev/null +++ b/src/ntpTimestamp.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include "stream.h" + +namespace MmtTlv { + +class NtpTimestamp { +public: + bool unpack(Common::ReadStream& stream); + int64_t toPcrValue() const; + int64_t toPtsValue() const; + + uint32_t seconds{}; + uint32_t fraction{}; +}; + +} diff --git a/src/remuxerHandler.cpp b/src/remuxerHandler.cpp index 62c4e81..4070e91 100644 --- a/src/remuxerHandler.cpp +++ b/src/remuxerHandler.cpp @@ -37,7 +37,10 @@ #include "timebase.h" #include "config.h" #include "ntp.h" -#include "b24SubtitleConvertor.h" +#include "b24SubtitleConverter.h" +#include +#include +#include namespace { @@ -141,16 +144,49 @@ void RemuxerHandler::onAudioData(const MmtTlv::MmtStream& mmtStream, const MmtTl } void RemuxerHandler::onSubtitleData(const MmtTlv::MmtStream& mmtStream, const struct MmtTlv::MfuData& mfuData) { + const auto& subtitleInfo = mmtStream.additionalAribSubtitleInfo(); + if (!subtitleInfo) { + return; + } + + // ARIB-TTML + if (subtitleInfo->subtitleFormat != 0) { + return; + } + + const bool synchronized = mmtStream.isClosedCaption(); + if ((!synchronized && !mmtStream.isSuperimposition()) || + (!synchronized && subtitleInfo->tmd != 0b1111)) { + return; + } + std::string ttml(mfuData.data.begin(), mfuData.data.end()); std::list output; - B24SubtitleConvertor::convert(ttml, output); + const auto pesType = synchronized ? B24::PESData::PESType::Synchronized : B24::PESData::PESType::Asynchronous; + B24SubtitleConverter::convert(ttml, output, pesType, subtitleInfo->resolution); if (output.empty()) { return; } + const auto currentPts = lastPcr / 300; + if (subtitleInfo->tmd == 0b1111) { + for (const auto& pesData : output) { + writeSubtitle(mmtStream, pesData, currentPts); + } + return; + } + + const auto referencePts = subtitleInfo->referenceStartTime.toPtsValue(); for (const auto& pesData : output) { - writeSubtitle(mmtStream, pesData); + if (!pesData.begin) { + continue; + } + const auto pts = referencePts + static_cast(*pesData.begin * 90); + if (pts < 0) { + continue; + } + writeSubtitle(mmtStream, pesData, std::max(static_cast(pts), currentPts)); } } @@ -284,23 +320,27 @@ void RemuxerHandler::writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTl } } -void RemuxerHandler::writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle) { - std::vector pesOutput; +void RemuxerHandler::writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, std::optional pts) { + const auto& subtitleInfo = mmtStream.additionalAribSubtitleInfo(); + if (!subtitleInfo) { + return; + } + const bool synchronized = mmtStream.isClosedCaption(); + if (!synchronized && !mmtStream.isSuperimposition()) { + return; + } + + std::vector pesOutput; PESPacket pes; - if (mmtStream.getComponentTag() == 0x30) { - uint64_t pts = subtitle.calcPts(programStartTime); - pts = std::max(pts, lastPcr / 300); - if (pts == 0) { - return; - } - pes.setPts(pts); + if (synchronized && pts) { + pes.setPts(*pts); } - pes.setStreamId(componentTagToStreamId(mmtStream.getComponentTag())); + pes.setStreamId(synchronized ? STREAM_ID_PRIVATE_STREAM_1 : STREAM_ID_PRIVATE_STREAM_2); pes.setPayload(&subtitle.pesData); pes.setPayloadLength(subtitle.pesData.size()); - if (mmtStream.getComponentTag() == 0x30) { + if (synchronized) { pes.setPrivateData(&ccis); pes.setStuffingByteLength(1); } @@ -355,43 +395,40 @@ void RemuxerHandler::writeCaptionManagementData(uint64_t pts) { if (stream.second.getAssetType() != MmtTlv::AssetType::stpp) { continue; } + const auto& subtitleInfo = stream.second.additionalAribSubtitleInfo(); + if (!subtitleInfo) { + continue; + } B24::CaptionManagementData captionManagementData; B24::CaptionManagementData::Language language; - language.languageCode = "jpn"; + language.languageCode = subtitleInfo->languageCode; language.format = 0b1000; - if (stream.second.getComponentTag() == 0x30) { - language.dmf = 0b1010; - } - else { - language.dmf = 0; - } + language.dmf = subtitleInfo->dmf; captionManagementData.languages.push_back(language); B24::DataGroup dataGroup; dataGroup.setGroupData(captionManagementData); B24::PESData pesData(dataGroup); - if (stream.second.getComponentTag() == 0x30) { - pesData.SetPESType(B24::PESData::PESType::Synchronized); - } - else { - pesData.SetPESType(B24::PESData::PESType::Asynchronous); + const bool synchronized = stream.second.isClosedCaption(); + if (!synchronized && !stream.second.isSuperimposition()) { + continue; } + pesData.SetPESType(synchronized ? B24::PESData::PESType::Synchronized : B24::PESData::PESType::Asynchronous); std::vector packedPesData; - pesData.pack(packedPesData); std::vector pesOutput; PESPacket pes; - if (stream.second.getComponentTag() == 0x30) { + if (synchronized) { pes.setPts(lastCaptionManagementDataPts); } - pes.setStreamId(componentTagToStreamId(stream.second.getComponentTag())); + pes.setStreamId(synchronized ? STREAM_ID_PRIVATE_STREAM_1 : STREAM_ID_PRIVATE_STREAM_2); pes.setPayload(&packedPesData); pes.setPayloadLength(packedPesData.size()); - if (stream.second.getComponentTag() == 0x30) { + if (synchronized) { pes.setPrivateData(&ccis); pes.setStuffingByteLength(1); } @@ -553,13 +590,6 @@ void RemuxerHandler::onMhBit(const MmtTlv::MhBit& mhBit) { void RemuxerHandler::onMhEit(const MmtTlv::MhEit& mhEit) { tsid = mhEit.tlvStreamId; - if (mhEit.isPf() && mhEit.sectionNumber == 0 && !mhEit.events.empty()) { - uint64_t startTime{}; - if (EITConvertStartTimeToUnixTime((mhEit.events.begin())->get()->startTime, &startTime)) { - programStartTime = startTime; - } - } - ts::EIT tsEit(true, mhEit.isPf(), 0, mhEit.versionNumber, true, mhEit.serviceId, mhEit.tlvStreamId, mhEit.originalNetworkId); for (const auto& mhEvent : mhEit.events) { ts::EIT::Event tsEvent(&tsEit); @@ -877,12 +907,7 @@ void RemuxerHandler::onMpt(const MmtTlv::Mpt& mpt) { else if (asset.assetType == MmtTlv::AssetType::stpp) { ts::DataComponentDescriptor descriptor; descriptor.data_component_id = 0x0008; - if (mmtStream->getComponentTag() == 0x30) { - descriptor.additional_data_component_info.push_back(0x3D); - } - else { - descriptor.additional_data_component_info.push_back(0x3C); - } + descriptor.additional_data_component_info.push_back(mmtStream->isClosedCaption() ? 0x3D : 0x3C); stream.descs.add(duck, descriptor); } @@ -1087,7 +1112,8 @@ void RemuxerHandler::onNtp(const MmtTlv::NTPv4& ntp) { lastPcr = ntp.transmit_timestamp.toPcrValue(); - writeCaptionManagementData(ntp.transmit_timestamp.toPcrValue() / 300); + const auto pts = static_cast(ntp.transmit_timestamp.toPtsValue()); + writeCaptionManagementData(pts); } void RemuxerHandler::clear() { @@ -1099,5 +1125,4 @@ void RemuxerHandler::clear() { tsid = -1; lastPcr = 0; lastCaptionManagementDataPts = 0; - programStartTime = 0; } diff --git a/src/remuxerHandler.h b/src/remuxerHandler.h index c25be3f..0e84746 100644 --- a/src/remuxerHandler.h +++ b/src/remuxerHandler.h @@ -1,10 +1,11 @@ #pragma once #include "demuxerHandler.h" -#include "b24SubtitleConvertor.h" -#include "damt.h" -#include -#include -#include +#include "b24SubtitleConverter.h" +#include "damt.h" +#include +#include +#include +#include namespace StreamType { @@ -78,26 +79,25 @@ class RemuxerHandler : public MmtTlv::DemuxerHandler { void clear(); private: - enum class PesState { - Init, - InFragment, - }; - - void writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTlv::MfuData& mfuData, const std::vector& data); - void writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle); - void writeCaptionManagementData(uint64_t pts); + enum class PesState { + Init, + InFragment, + }; + void writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTlv::MfuData& mfuData, const std::vector& data); + void writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, + std::optional pts = std::nullopt); + void writeCaptionManagementData(uint64_t pts); MmtTlv::MmtTlvDemuxer& demuxer; OutputCallback outputCallback; std::unordered_map mapService2Pid; std::unordered_map mapCC; std::unordered_map> mapPesPendingData; - std::unordered_map mapPesPacketIndex; - std::unordered_map mapPesState; - int tsid{-1}; - uint64_t lastPcr{}; - uint64_t lastCaptionManagementDataPts{}; - uint64_t programStartTime{}; - inline static const std::vector ccis = { 0x43, 0x43, 0x49, 0x53, 0x01, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; + std::unordered_map mapPesPacketIndex; + std::unordered_map mapPesState; + int tsid{-1}; + uint64_t lastPcr{}; + uint64_t lastCaptionManagementDataPts{}; + inline static const std::vector ccis = { 0x43, 0x43, 0x49, 0x53, 0x01, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; ts::DuckContext duck; -}; \ No newline at end of file +}; diff --git a/src/ttml.cpp b/src/ttml.cpp deleted file mode 100644 index 5bdb1e2..0000000 --- a/src/ttml.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "ttml.h" -#include "pugixml.hpp" - -namespace { - -uint64_t parseTimestamp(const std::string& timestamp) { - size_t dotPos = timestamp.find('.'); - uint64_t hours, minutes, seconds, millis = 0; - - if (dotPos != std::string::npos) { - if (timestamp.size() < 10) { - throw std::invalid_argument("Invalid timestamp format: " + timestamp); - } - if (timestamp[2] != ':' || timestamp[5] != ':') { - throw std::invalid_argument("Invalid timestamp format: " + timestamp); - } - hours = std::stoull(timestamp.substr(0, 2)); - minutes = std::stoull(timestamp.substr(3, 2)); - seconds = std::stoull(timestamp.substr(6, 2)); - std::string frac = timestamp.substr(dotPos + 1); - if (frac.empty() || frac.size() > 3) { - throw std::invalid_argument("Invalid fractional seconds in timestamp: " + timestamp); - } - if (frac.size() == 1) { - millis = std::stoull(frac) * 100; - } - else if (frac.size() == 2) { - millis = std::stoull(frac) * 10; - } - else { - millis = std::stoull(frac); - } - } - else { - if (timestamp.size() != 8 || timestamp[2] != ':' || timestamp[5] != ':') { - throw std::invalid_argument("Invalid timestamp format: " + timestamp); - } - hours = std::stoull(timestamp.substr(0, 2)); - minutes = std::stoull(timestamp.substr(3, 2)); - seconds = std::stoull(timestamp.substr(6, 2)); - } - - return hours * 3600 * 1000ULL + minutes * 60 * 1000ULL + seconds * 1000ULL + millis; -} - -} - -TTML TTMLPaser::parse(const std::string& input) { - TTML output; - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_buffer(input.data(), input.size()); - if (result.status != pugi::status_ok) { - return {}; - } - - for (pugi::xml_node p : doc.child("tt").child("head").child("layout").children("region")) { - TTMLRegion region; - region.id = p.attribute("xml:id").value(); - if (p.attribute("tts:extent")) { - region.extent = TTMLCssValueParser::parsePair(p.attribute("tts:extent").value()); - } - if (p.attribute("tts:origin")) { - region.origin = TTMLCssValueParser::parsePair(p.attribute("tts:origin").value()); - } - output.regions.push_back(region); - } - - for (pugi::xml_node p : doc.child("tt").child("head").child("styling").children("style")) { - TTMLStyle style; - style.id = p.attribute("xml:id").value(); - if (p.attribute("tts:fontSize")) { - style.fontSize = TTMLCssValueParser::parsePair(p.attribute("tts:fontSize").value()); - } - if (p.attribute("tts:lineHeight")) { - style.lineHeight = TTMLCssValueParser::parse(p.attribute("tts:lineHeight").value()); - } - if (p.attribute("tts:fontWeight")) { - style.fontWeight = TTMLCssValueParser::parse(p.attribute("tts:fontWeight").value()); - } - if (p.attribute("tts:fontStyle")) { - style.fontStyle = TTMLCssValueParser::parse(p.attribute("tts:fontStyle").value()); - } - if (p.attribute("tts:color")) { - style.color = TTMLCssValueParser::parse(p.attribute("tts:color").value()); - } - if (p.attribute("tts:backgroundColor")) { - style.backgroundColor = TTMLCssValueParser::parse(p.attribute("tts:backgroundColor").value()); - } - - output.styles.push_back(style); - } - - for (pugi::xml_node div : doc.child("tt").child("body").children("div")) { - TTMLDivTag divTag; - if (div.attribute("begin")) { - divTag.begin = parseTimestamp(div.attribute("begin").value()); - } - if (div.attribute("end")) { - divTag.end = parseTimestamp(div.attribute("end").value()); - } - - for (pugi::xml_node p : div.children("p")) { - std::string regionId = p.attribute("region").value(); - auto region = std::find_if(output.regions.begin(), output.regions.end(), [regionId](const TTMLRegion& r) { - return r.id == regionId; - }); - - TTMLPTag pTag; - pTag.id = p.attribute("xml:id").value(); - if (region != output.regions.end()) { - pTag.region = *region; - } - - for (pugi::xml_node span : p.children("span")) { - TTMLSpanTag spanTag; - spanTag.id = span.attribute("xml:id").value(); - spanTag.text = span.text().get(); - - std::string styleStr = span.attribute("style").value(); - std::istringstream styleStream(styleStr); - std::string styleId; - while (styleStream >> styleId) { - auto style = std::find_if(output.styles.begin(), output.styles.end(), [styleId](const TTMLStyle& s) { - return s.id == styleId; - }); - if (style != output.styles.end()) { - if (style->fontSize.has_value()) { - spanTag.style.fontSize = style->fontSize; - } - if (style->lineHeight.has_value()) { - spanTag.style.lineHeight = style->lineHeight; - } - if (style->fontWeight.has_value()) { - spanTag.style.fontWeight = style->fontWeight; - } - if (style->fontStyle.has_value()) { - spanTag.style.fontStyle = style->fontStyle; - } - if (style->color.has_value()) { - spanTag.style.color = style->color; - } - if (style->backgroundColor.has_value()) { - spanTag.style.backgroundColor = style->backgroundColor; - } - } - } - - pTag.spanTags.push_back(spanTag); - } - - divTag.pTags.push_back(pTag); - } - - output.divTags.push_back(divTag); - } - - return output; -} - -TTMLCssValue TTMLCssValueParser::parse(const std::string& input) { - try { - if (input.find("px") != std::string::npos || input.find("em") != std::string::npos || - input.find("rem") != std::string::npos || input.find("%") != std::string::npos) { - std::regex regex(R"(^([+-]?\d*\.?\d+)(px|em|rem|%)$)"); - std::smatch match; - if (std::regex_match(input, match, regex)) { - return TTMLCssValue(TTMLCssValueLength(std::stof(match[1]), match[2])); - } - else { - throw std::invalid_argument("Invalid length value: " + input); - } - } - - if (input.find("#") == 0) { - return TTMLCssValue(TTMLCssValueColor(input)); - } - - static const std::set validKeywords = { "bold", "italic", "normal", "none" }; - if (validKeywords.find(input) != validKeywords.end()) { - return TTMLCssValue(TTMLCssValueKeyword(input)); - } - - return TTMLCssValue(TTMLCssValueNumber(std::stof(input))); - - } - catch (const std::invalid_argument& e) { - std::cerr << e.what() << std::endl; - throw; - } -} - -TTMLCssValuePair TTMLCssValueParser::parsePair(const std::string& input) { - std::istringstream iss(input); - std::string token1, token2; - if (!(iss >> token1 >> token2)) { - throw std::invalid_argument("Failed to parse TTML value pair from: " + input); - } - TTMLCssValue value1 = parse(token1); - TTMLCssValue value2 = parse(token2); - return std::make_pair(value1, value2); -} \ No newline at end of file diff --git a/src/ttml.h b/src/ttml.h deleted file mode 100644 index aabd147..0000000 --- a/src/ttml.h +++ /dev/null @@ -1,136 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class TTMLCssValueLength { -public: - TTMLCssValueLength(float v, const std::string& u) : value(v), unit(u) {} - - float value; - std::string unit; - -}; - -class TTMLCssValueColor { -public: - TTMLCssValueColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : r(r), g(g), b(b), a(a) {} - - TTMLCssValueColor(const std::string& hex) { - if (hex.size() != 9 || hex[0] != '#') { - throw std::invalid_argument("Invalid color format. Expected format: #RRGGBBAA"); - } - - unsigned long value = std::stoul(hex.substr(1), nullptr, 16); - r = static_cast((value >> 24) & 0xFF); - g = static_cast((value >> 16) & 0xFF); - b = static_cast((value >> 8) & 0xFF); - a = static_cast(value & 0xFF); - } - - uint8_t r; - uint8_t g; - uint8_t b; - uint8_t a; -}; - -class TTMLCssValueKeyword { -public: - TTMLCssValueKeyword(const std::string& k) : keyword(k) {} - - std::string keyword; -}; - -class TTMLCssValueNumber { -public: - TTMLCssValueNumber(float n) : number(n) {} - - float number; -}; - -class TTMLCssValue { -public: - using ValueType = std::variant; - -private: - ValueType value; - -public: - TTMLCssValue(TTMLCssValueLength length) : value(length) {} - TTMLCssValue(TTMLCssValueColor color) : value(color) {} - TTMLCssValue(TTMLCssValueKeyword keyword) : value(keyword) {} - TTMLCssValue(TTMLCssValueNumber number) : value(number) {} - - template - T getValue() const { - return std::get(value); - } - - template - void setValue(const T& new_value) { - value = new_value; - } -}; - -using TTMLCssValuePair = std::pair; - -class TTMLCssValueParser { -public: - static TTMLCssValue parse(const std::string& input); - static TTMLCssValuePair parsePair(const std::string& input); - -}; - -struct TTMLRegion { - std::string id; - std::optional> extent; - std::optional> origin; -}; - -struct TTMLStyle { - std::string id; - std::optional fontSize; - std::optional lineHeight; - std::optional fontWeight; - std::optional fontStyle; - std::optional color; - std::optional backgroundColor; -}; - -struct TTMLSpanTag { - std::string id; - std::string text; - TTMLStyle style; -}; - -struct TTMLPTag { - std::string id; - TTMLRegion region; - std::list spanTags; -}; - -struct TTMLDivTag { - std::optional begin; - std::optional end; - std::list pTags; -}; - -struct TTML { - std::list divTags; - std::list regions; - std::list styles; - -}; - -class TTMLPaser { -public: - static TTML parse(const std::string& input); - -}; \ No newline at end of file diff --git a/src/ttml/ast.h b/src/ttml/ast.h new file mode 100644 index 0000000..4729390 --- /dev/null +++ b/src/ttml/ast.h @@ -0,0 +1,65 @@ +#pragma once +#include "ttml/style.h" +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +namespace ast { + +struct Timing { + std::optional begin; + std::optional end; +}; + +struct StyleDefinition { + std::string id; + std::vector style_refs; + StyleProperties style; +}; + +struct RegionDefinition { + std::string id; + std::vector style_refs; + StyleProperties style; +}; + +struct LineBreak {}; +using SpanContent = std::variant; + +struct Span { + std::optional id; + Timing timing; + std::optional region; + std::vector style_refs; + std::vector content; +}; + +struct Paragraph { + std::optional id; + Timing timing; + std::optional region; + std::vector spans; +}; + +struct Division { + Timing timing; + std::vector paragraphs; +}; + +struct Document { + std::vector styles; + std::vector regions; + std::optional division; +}; + +} // namespace ast + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/b24_converter.cpp b/src/ttml/b24_converter.cpp new file mode 100644 index 0000000..5ef7d36 --- /dev/null +++ b/src/ttml/b24_converter.cpp @@ -0,0 +1,818 @@ +#include "ttml/b24_converter.h" +#include "aribTextEncoder.h" +#include "b24Color.h" +#include "b24ControlSet.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +namespace { + +constexpr double kB24LayoutWidth = 960.0; + +constexpr double sourceLayoutWidth(MmtTlv::SubtitleResolution resolution) { + switch (resolution) { + case MmtTlv::SubtitleResolution::Resolution2K: + return 1920.0; + case MmtTlv::SubtitleResolution::Resolution4K: + return 3840.0; + case MmtTlv::SubtitleResolution::Resolution8K: + return 7680.0; + } + + return 3840.0; +} + +struct StyledRun { + std::string_view text; + const StyleProperties* style; +}; + +struct Line { + std::vector runs; +}; + +struct RegionBlock { + const resolved::Region* region; + std::vector lines; +}; + +struct Page { + std::vector blocks; +}; + +struct SpanCue { + const resolved::Paragraph* paragraph; + const resolved::Span* span; + const resolved::Region* region; + size_t order; +}; + +enum class SpanRegionMode { + None, + StyleOnly, + Positioned, +}; + +struct CollectResult { + std::vector cues; + std::optional error; +}; + +struct TimelineEvent { + std::chrono::milliseconds time; + std::vector starts; + std::vector ends; +}; + +struct RenderAction { + std::optional begin; + uint64_t delay; + bool clear; + Page page; +}; + +uint64_t to_deciseconds(std::chrono::milliseconds time) { + return static_cast(time.count() / 100); +} + +std::vector split_by_null(std::string_view data) { + std::vector output; + output.reserve(static_cast(std::ranges::count(data, '\0'))); + size_t begin = 0; + + for (size_t i = 0; i < data.size(); ++i) { + if (data[i] == '\0') { + output.emplace_back(data.substr(begin, i - begin)); + begin = i + 1; + } + } + + if (begin < data.size()) { + output.emplace_back(data.substr(begin)); + } + return output; +} + +void append_span(RegionBlock& block, const resolved::Span& span) { + if (block.lines.empty()) { + block.lines.emplace_back(); + } + + for (const auto& content : span.content) { + if (const auto* text = std::get_if(&content)) { + if (!text->empty()) { + block.lines.back().runs.push_back(StyledRun{ + .text = *text, + .style = &span.style, + }); + } + } + else { + block.lines.emplace_back(); + } + } +} + +class CueCollector { +public: + CueCollector(const resolved::Document& document, SyncMode mode) + : document_(document), mode_(mode) { + } + + CollectResult run() const { + CollectResult output; + size_t order = 0; + + if (!document_.division) { + return output; + } + + for (const auto& paragraph : document_.division->paragraphs) { + // ARIB-TTML has no initial font size, so every span must resolve one. + const bool has_missing_font_size = std::ranges::any_of(paragraph.spans, + [](const resolved::Span& span) { + return !span.style.font_size; + } + ); + if (has_missing_font_size) { + continue; + } + + // ARIB-TTML requires all spans to use the same region reference. + std::optional paragraph_region_mode; + bool has_mixed_region_modes = false; + for (const auto& span : paragraph.spans) { + const bool has_origin = span.region && span.region->style.origin.has_value(); + const bool has_extent = span.region && span.region->style.extent.has_value(); + if (has_origin != has_extent) { + // The existing validation below reports an incomplete origin/extent pair. + continue; + } + + const auto region_mode = !span.region ? SpanRegionMode::None : + (has_origin ? SpanRegionMode::Positioned : SpanRegionMode::StyleOnly); + if (!paragraph_region_mode) { + paragraph_region_mode = region_mode; + } + else if (*paragraph_region_mode != region_mode) { + has_mixed_region_modes = true; + break; + } + } + if (has_mixed_region_modes) { + continue; + } + + for (const auto& span : paragraph.spans) { + if (mode_ == SyncMode::Sync && span.timing.begin && span.timing.end && + *span.timing.end <= *span.timing.begin) { + continue; + } + + const bool has_text = std::ranges::any_of(span.content, + [](const auto& content) { + const auto* text = std::get_if(&content); + return text && !text->empty(); + } + ); + if (!has_text) { + continue; + } + + const resolved::Region* paragraph_region = nullptr; + if (paragraph.region) { + const bool has_origin = paragraph.region->style.origin.has_value(); + const bool has_extent = paragraph.region->style.extent.has_value(); + if (has_origin != has_extent) { + output.error = B24ConvertError{ + "paragraph region must specify both tts:origin and tts:extent" + }; + return output; + } + if (has_origin) { + paragraph_region = &*paragraph.region; + } + } + + const resolved::Region* region = nullptr; + if (span.region) { + const bool has_origin = span.region->style.origin.has_value(); + const bool has_extent = span.region->style.extent.has_value(); + if (has_origin != has_extent) { + output.error = B24ConvertError{ + "span region must specify both tts:origin and tts:extent" + }; + return output; + } + if (has_origin) { + region = &*span.region; + } + } + + if (!region) { + // A span region without geometry is style-only and continues in the paragraph region. + region = paragraph_region; + } + + if (!region) { + output.error = B24ConvertError{ "span has no effective region" }; + return output; + } + + output.cues.push_back(SpanCue{ + .paragraph = ¶graph, + .span = &span, + .region = region, + .order = order++, + }); + } + } + + return output; + } + +private: + const resolved::Document& document_; + SyncMode mode_; + +}; + +class TimelineBuilder { +public: + explicit TimelineBuilder(const std::vector& cues) + : cues_(cues) { + } + + std::vector run() const { + std::map events; + + for (const auto& cue : cues_) { + const auto begin = cue.span->timing.begin.value_or(std::chrono::milliseconds{}); + auto& start_event = events[begin]; + start_event.time = begin; + start_event.starts.push_back(&cue); + + if (cue.span->timing.end) { + const auto end = *cue.span->timing.end; + auto& end_event = events[end]; + end_event.time = end; + end_event.ends.push_back(&cue); + } + } + + std::vector output; + output.reserve(events.size()); + for (auto& entry : events) { + output.push_back(std::move(entry.second)); + } + return output; + } + +private: + const std::vector& cues_; +}; + +class Encoder { +public: + Encoder(const std::vector& cues, SyncMode mode, MmtTlv::SubtitleResolution resolution) + : cues_(cues), mode_(mode), layoutScale_(kB24LayoutWidth / sourceLayoutWidth(resolution)) { + } + + B24ConvertResult run() { + if (mode_ == SyncMode::Async) { + return encode_async(); + } + else { + return encode_sync(); + } + } + +private: + static Page build_page(std::span active_cues) { + Page page; + const resolved::Paragraph* current_paragraph = nullptr; + const resolved::Region* current_region = nullptr; + RegionBlock* block = nullptr; + + for (const auto* cue : active_cues) { + if (!block || cue->paragraph != current_paragraph || cue->region != current_region) { + page.blocks.push_back(RegionBlock{ + .region = cue->region, + .lines = { Line{} }, + }); + block = &page.blocks.back(); + current_paragraph = cue->paragraph; + current_region = cue->region; + } + + append_span(*block, *cue->span); + } + + return page; + } + + B24ConvertResult encode_async() { + std::vector cues; + cues.reserve(cues_.size()); + for (const auto& cue : cues_) { + cues.push_back(&cue); + } + + const auto page = build_page(cues); + auto encoded_runs = encode_text(page); + size_t encoded_run_index = 0; + append_initial_format(); + encode_page(page, encoded_runs, encoded_run_index); + + return { + .outputs = { B24ConvertOutput{ + .data = std::move(output_), + .begin = {}, + .clear = false, + } }, + .error = {}, + }; + } + + B24ConvertResult encode_sync() { + const auto actions = build_render_actions(); + if (actions.empty()) { + return { + .outputs = { B24ConvertOutput{ + .data = {}, + .begin = {}, + .clear = true, + } }, + .error = {}, + }; + } + + B24ConvertResult result; + result.outputs.reserve(actions.size()); + for (size_t i = 0; i < actions.size(); ++i) { + const auto& action = actions[i]; + const bool is_last_clear = i + 1 == actions.size() + && action.clear + && action.page.blocks.empty() + && !result.outputs.empty(); + if (is_last_clear) { + output_ = std::move(result.outputs.back().data); + append_time(action.delay); + append_clear(); + result.outputs.back().data = std::move(output_); + continue; + } + + auto encoded_runs = encode_text(action.page); + size_t encoded_run_index = 0; + output_.clear(); + reset_state(); + append_initial_format(); + + encode_page(action.page, encoded_runs, encoded_run_index); + + result.outputs.push_back(B24ConvertOutput{ + .data = std::move(output_), + .begin = action.begin, + .clear = action.clear, + }); + } + + return result; + } + + std::vector build_render_actions() const { + std::vector actions; + std::vector active_cues; + std::chrono::milliseconds current_time{}; + bool first_event = true; + + active_cues.reserve(cues_.size()); + + const auto events = TimelineBuilder{cues_}.run(); + actions.reserve(events.size()); + for (const auto& event : events) { + const auto delay = first_event ? 0 : to_deciseconds(event.time) - to_deciseconds(current_time); + + for (const auto* cue : event.ends) { + std::erase(active_cues, cue); + } + for (const auto* cue : event.starts) { + const auto position = std::ranges::lower_bound(active_cues, cue->order, {}, + [](const SpanCue* active_cue) { + return active_cue->order; + } + ); + active_cues.insert(position, cue); + } + + const bool starts_without_time = std::ranges::any_of(event.starts, + [](const SpanCue* cue) { + return !cue->span->timing.begin; + } + ); + actions.push_back(RenderAction{ + .begin = first_event && starts_without_time ? std::nullopt : std::optional{event.time}, + .delay = delay, + .clear = !first_event, + .page = build_page(active_cues), + }); + + current_time = event.time; + first_event = false; + } + return actions; + } + + static void collect_text(const Page& page, std::string& output) { + for (const auto& block : page.blocks) { + for (const auto& line : block.lines) { + for (const auto& run : line.runs) { + output.append(run.text); + output.push_back('\0'); + } + } + } + } + + static std::vector encode_text(const Page& page) { + std::string text; + collect_text(page, text); + + return split_by_null(arib::text::encode(text, arib::charset::EncodeMode::Caption)); + } + + void encode_page(const Page& page, const std::vector& encoded_runs, size_t& encoded_run_index) { + for (const auto& block : page.blocks) { + if (block.region->style.extent) { + append_extent(*block.region->style.extent); + } + + if (block.region->style.origin) { + append_origin(*block.region->style.origin); + } + + bool aps = false; + for (size_t line_index = 0; line_index < block.lines.size(); ++line_index) { + const auto& line = block.lines[line_index]; + for (const auto& run : line.runs) { + const auto& style = *run.style; + if (style.letter_spacing) { + const auto spacing = static_cast(std::max(0.0, std::round(*style.letter_spacing * layoutScale_))); + set_character_spacing(spacing); + } + + if (style.line_height && style.font_size) { + const double font_width = style.font_size->x; + const double font_height = style.font_size->y; + const double line_height = std::holds_alternative(*style.line_height) + ? font_height * 1.2 + : std::get(*style.line_height); + const double character_scale = font_width == 72 && font_height == 72 ? 0.5 : 1.0; + const auto spacing = static_cast(std::max(0.0, std::round( + (line_height - font_height) * layoutScale_ / character_scale))); + set_line_spacing(spacing); + } + + if (style.font_size) { + const double font_width = style.font_size->x; + const double font_height = style.font_size->y; + uint32_t composition_width = static_cast(std::round(144 * layoutScale_)); + uint32_t composition_height = static_cast(std::round(144 * layoutScale_)); + uint8_t character_size = B24ControlSet::NSZ; + if (font_width == 72 && font_height == 144) { + character_size = B24ControlSet::MSZ; + } + else if (font_width == 72 && font_height == 72) { + character_size = B24ControlSet::SSZ; + } + else { + composition_width = static_cast(std::max(1.0, std::round(font_width * layoutScale_))); + composition_height = static_cast(std::max(1.0, std::round(font_height * layoutScale_))); + } + set_character_composition(composition_width, composition_height); + set_character_size(character_size); + } + + if (!style.text_outline || std::holds_alternative(*style.text_outline)) { + set_ornament_off(); + } + else { + const auto& text_outline = std::get(*style.text_outline); + const auto color = text_outline.color.value_or( + style.color.value_or(StyleColorValue{ { 255, 255, 255, 255 } })); + const auto [palette, index] = findClosestColor(ColorRGBA{ + color.r(), color.g(), color.b(), color.a()}); + set_ornament(palette, index); + } + + if (style.color) { + set_text_color(*style.color); + } + + if (style.background_color) { + set_background_color(*style.background_color); + } + + const bool underline = style.text_decoration == StyleTextDecorationValue::Underline; + set_underline(underline); + + uint8_t font = 0; + if (style.font_weight == StyleFontWeightValue::Bold) { + font |= 0x01; + } + if (style.font_style == StyleFontStyleValue::Italic) { + font |= 0x02; + } + set_font(font); + + + if (!aps) { + append_aps(0, 0); + aps = true; + } + const auto& encoded = encoded_runs.at(encoded_run_index++); + output_.insert(output_.end(), encoded.begin(), encoded.end()); + } + if (line_index + 1 < block.lines.size()) { + output_.push_back(B24ControlSet::APR); + } + } + } + } + + void reset_state() { + last_text_color_palette_ = 0; + last_text_color_index_ = 7; + last_background_color_palette_ = 0; + last_background_color_index_ = 8; + character_size_ = B24ControlSet::NSZ; + character_composition_width_ = 36; + character_composition_height_ = 36; + line_spacing_ = 24; + character_spacing_ = 4; + underline_ = false; + font_ = 0; + ornament_enabled_ = false; + ornament_palette_ = 0; + ornament_index_ = 0; + } + + void append_clear() { + output_.push_back(B24ControlSet::CS); + reset_state(); + } + + void append_initial_format() { + output_.push_back(B24ControlSet::CSI); + output_.push_back(0x37); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::SWF); + } + + void append_number(int n) { + n = std::max(n, 0); + + std::array::digits10 + 1> buffer; + const auto result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), n); + for (auto current = buffer.data(); current != result.ptr; ++current) { + output_.push_back(static_cast(*current)); + } + } + + void append_extent(const LengthPair& extent) { + output_.push_back(B24ControlSet::CSI); + append_number(static_cast(extent.x * layoutScale_)); + output_.push_back(0x3B); + append_number(static_cast(extent.y * layoutScale_)); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::SDF); + } + + void append_origin(const LengthPair& origin) { + output_.push_back(B24ControlSet::CSI); + append_number(static_cast(std::lround(origin.x * layoutScale_))); + output_.push_back(0x3B); + append_number(static_cast(std::lround(origin.y * layoutScale_))); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::SDP); + } + + void append_aps(uint8_t row, uint8_t column) { + output_.push_back(B24ControlSet::APS); + output_.push_back(static_cast(0x40 | row)); + output_.push_back(static_cast(0x40 | column)); + } + + void set_character_composition(uint32_t width, uint32_t height) { + if (character_composition_width_ == width && + character_composition_height_ == height) { + return; + } + + output_.push_back(B24ControlSet::CSI); + append_number(width); + output_.push_back(0x3B); + append_number(height); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::SSM); + + character_composition_width_ = width; + character_composition_height_ = height; + } + + void set_character_size(uint8_t size) { + if (character_size_ == size) { + return; + } + + output_.push_back(size); + character_size_ = size; + } + + void append_spacing(uint32_t spacing, uint8_t control) { + output_.push_back(B24ControlSet::CSI); + append_number(spacing); + output_.push_back(B24ControlSet::SP); + output_.push_back(control); + } + + void set_line_spacing(uint32_t spacing) { + if (line_spacing_ == spacing) { + return; + } + + append_spacing(spacing, B24ControlSet::SVS); + line_spacing_ = spacing; + } + + void set_character_spacing(uint32_t spacing) { + if (character_spacing_ == spacing) { + return; + } + + append_spacing(spacing, B24ControlSet::SHS); + character_spacing_ = spacing; + } + + void set_underline(bool underline) { + if (underline_ == underline) { + return; + } + + output_.push_back(underline ? B24ControlSet::STL : B24ControlSet::SPL); + underline_ = underline; + } + + void set_text_color(const StyleColorValue& color) { + const auto [palette, index] = findClosestColor(ColorRGBA{ + color.r(), color.g(), color.b(), color.a() + }); + if (last_text_color_palette_ == palette && last_text_color_index_ == index) { + return; + } + + output_.push_back(B24ControlSet::COL); + output_.push_back(0x20); + output_.push_back(static_cast(0x40 | palette)); + if (index <= 7) { + output_.push_back(static_cast(B24ControlSet::BKF + index)); + } + else { + output_.push_back(B24ControlSet::COL); + output_.push_back(static_cast(0x40 | index)); + } + + last_text_color_palette_ = palette; + last_text_color_index_ = index; + } + + void set_background_color(const StyleColorValue& color) { + const auto [palette, index] = findClosestColor(ColorRGBA{ + color.r(), color.g(), color.b(), color.a() + }); + if (last_background_color_palette_ == palette && + last_background_color_index_ == index) { + return; + } + + output_.push_back(B24ControlSet::COL); + output_.push_back(0x20); + output_.push_back(static_cast(0x40 | palette)); + output_.push_back(B24ControlSet::COL); + output_.push_back(static_cast(0x50 | index)); + + last_background_color_palette_ = palette; + last_background_color_index_ = index; + } + + void append_two_digits(uint8_t value) { + output_.push_back(static_cast(0x30 + value / 10)); + output_.push_back(static_cast(0x30 + value % 10)); + } + + void set_ornament(uint8_t palette, uint8_t index) { + if (ornament_enabled_ && ornament_palette_ == palette && ornament_index_ == index) { + return; + } + + output_.push_back(B24ControlSet::CSI); + output_.push_back(0x31); + output_.push_back(0x3B); + append_two_digits(palette); + append_two_digits(index); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::ORN); + + ornament_enabled_ = true; + ornament_palette_ = palette; + ornament_index_ = index; + } + + void set_ornament_off() { + if (!ornament_enabled_) { + return; + } + + output_.push_back(B24ControlSet::CSI); + output_.push_back(0x30); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::ORN); + + ornament_enabled_ = false; + } + + void set_font(uint8_t font) { + if (font_ == font) { + return; + } + + output_.push_back(B24ControlSet::CSI); + output_.push_back(0x30 + font); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::MDF); + + font_ = font; + } + + void append_time(std::uint64_t duration) { + while (duration > 0) { + const auto value = static_cast(std::min(duration, 0x3F)); + output_.push_back(B24ControlSet::TIME); + output_.push_back(0x20); + output_.push_back(0x40 | value); + duration -= value; + } + } + +private: + const std::vector& cues_; + SyncMode mode_; + double layoutScale_; + std::vector output_; + uint8_t last_text_color_palette_{0}; + uint8_t last_text_color_index_{7}; + uint8_t last_background_color_palette_{0}; + uint8_t last_background_color_index_{8}; + uint8_t character_size_{B24ControlSet::NSZ}; + uint32_t character_composition_width_{36}; + uint32_t character_composition_height_{36}; + uint32_t line_spacing_{24}; + uint32_t character_spacing_{4}; + bool underline_{false}; + uint8_t font_{0}; + bool ornament_enabled_{false}; + uint8_t ornament_palette_{0}; + uint8_t ornament_index_{0}; + +}; + +} // namespace + +B24ConvertResult convert_to_b24(const resolved::Document& document, SyncMode mode, MmtTlv::SubtitleResolution resolution) { + auto collected = CueCollector{document, mode}.run(); + if (collected.error) { + return { + .outputs = {}, + .error = std::move(collected.error), + }; + } + + return Encoder{collected.cues, mode, resolution}.run(); +} + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/b24_converter.h b/src/ttml/b24_converter.h new file mode 100644 index 0000000..29c325e --- /dev/null +++ b/src/ttml/b24_converter.h @@ -0,0 +1,40 @@ +#pragma once + +#include "ttml/resolved.h" +#include "ttml/sync_mode.h" +#include "additionalAribSubtitleInfo.h" + +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +struct B24ConvertError { + std::string message; +}; + +struct B24ConvertOutput { + std::vector data; + std::optional begin; + bool clear; +}; + +struct B24ConvertResult { + std::vector outputs; + std::optional error; + + [[nodiscard]] bool has_error() const noexcept { + return error.has_value(); + } +}; + +[[nodiscard]] B24ConvertResult convert_to_b24(const resolved::Document& document, SyncMode mode, MmtTlv::SubtitleResolution resolution); + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/parser.cpp b/src/ttml/parser.cpp new file mode 100644 index 0000000..da0b94d --- /dev/null +++ b/src/ttml/parser.cpp @@ -0,0 +1,632 @@ +#include "parser.h" +#include "ast.h" +#include "string_utils.h" +#include "pugixml.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +namespace { + +enum class StyleScope { + Content, + Region, +}; + +SourceLocation get_source_location(const pugi::xml_node& node, std::string_view source) { + const auto offset = node.offset_debug(); + + if (offset < 0) { + return { 0, 1, 1 }; + } + + uint32_t line = 1; + uint32_t column = 1; + + const auto end = std::min(static_cast(offset), source.size()); + for (size_t i = 0; i < end; ++i) { + if (source[i] == '\n') { + ++line; + column = 1; + } + else { + ++column; + } + } + + return { + .byte_offset = static_cast(offset), + .line = line, + .column = column, + }; +} + +std::optional parse_time_expression(std::string_view value) { + if (value.size() != 12 || value[2] != ':' || value[5] != ':' || value[8] != '.') { + return {}; + } + + auto parse_number = [](std::string_view s) -> std::optional { + int result = 0; + + for (const char c : s) { + if (c < '0' || c > '9') { + return {}; + } + + result = result * 10 + (c - '0'); + } + + return result; + }; + + const auto h = parse_number(value.substr(0, 2)); + const auto m = parse_number(value.substr(3, 2)); + const auto s = parse_number(value.substr(6, 2)); + const auto ms = parse_number(value.substr(9, 3)); + if (!h || !m || !s || !ms) { + return {}; + } + + if (*h >= 24 || *m >= 60 || *s >= 60) { + return {}; + } + + return std::chrono::hours{*h} + + std::chrono::minutes{*m} + + std::chrono::seconds{*s} + + std::chrono::milliseconds{*ms}; +} + +class Parser { +public: + Parser(std::string_view xml, SyncMode mode) + : xml_(xml), mode_(mode) { + } + + ParseResult run() { + ParseResult result; + pugi::xml_document doc; + pugi::xml_parse_result xml = doc.load_buffer(xml_.data(), xml_.size()); + if (xml.status != pugi::status_ok) { + return ParseResult{ + .document = {}, + .error = make_error(std::string("malformed xml: ") + xml.description()), + }; + } + + pugi::xml_node root; + for (auto child = doc.first_child(); !child.empty(); child = child.next_sibling()) { + if (child.type() == pugi::node_element) { + root = child; + break; + } + } + + const std::string_view root_name = root.name(); + if (root_name != "tt") { + return ParseResult{ + .document = {}, + .error = make_error("root element must be tt", root) + }; + } + + pugi::xml_node head; + pugi::xml_node body; + for (auto child = root.first_child(); !child.empty(); child = child.next_sibling()) { + const std::string_view child_name = child.name(); + if (child_name == "head") { + if (head.empty()) { + head = child; + } + else { + return ParseResult{ + .document = {}, + .error = make_error("only one head element is allowed", child) + }; + } + } + else if (child_name == "body") { + if (body.empty()) { + body = child; + } + else { + return ParseResult{ + .document = {}, + .error = make_error("only one body element is allowed", child) + }; + } + } + } + + if (!head.empty()) { + const auto error = parse_head(head); + if (error) { + return ParseResult{ + .document = {}, + .error = error + }; + } + } + if (!body.empty()) { + const auto error = parse_body(body); + if (error) { + return ParseResult{ + .document = {}, + .error = error + }; + } + } + + result.document = std::move(ast_doc_); + return result; + } + +private: + std::optional parse_body(pugi::xml_node body) { + for (auto child = body.first_child(); !child.empty(); child = child.next_sibling()) { + const std::string_view child_name = child.name(); + if (child_name == "div") { + if (ast_doc_.division) { + return make_error("only one div element is allowed in body element", child); + } + + const auto error = parse_div(child); + if (error) { + return error; + } + } + + } + + return {}; + } + + std::optional parse_div(pugi::xml_node body) { + ast::Division division; + const auto timing_error = parse_timing(body, division.timing); + if (timing_error) { + return timing_error; + } + + for (auto child = body.first_child(); !child.empty(); child = child.next_sibling()) { + const std::string_view child_name = child.name(); + if (child_name == "p") { + const auto paragraph_error = parse_p(child, division); + if (paragraph_error) { + return paragraph_error; + } + } + } + + ast_doc_.division = std::move(division); + + return {}; + } + + std::optional parse_p(pugi::xml_node node, ast::Division& division) { + ast::Paragraph paragraph; + + if (const auto id_attr = node.attribute("xml:id")) { + const std::string_view id = id_attr.value(); + if (id.empty()) { + return make_error("xml:id must not be empty", node); + } + + if (!register_id(id)) { + return make_error("duplicate xml:id", node); + } + + paragraph.id = id; + } + + if (const auto error = parse_timing(node, paragraph.timing)) { + return error; + } + if (const auto error = parse_region_ref(node, paragraph.region)) { + return error; + } + + for (auto child = node.first_child(); !child.empty(); child = child.next_sibling()) { + std::string_view child_name = child.name(); + if (child_name == "span") { + const auto error = parse_span(child, paragraph); + if (error) { + return error; + } + } + } + + division.paragraphs.push_back(std::move(paragraph)); + + return {}; + } + + std::optional parse_span(pugi::xml_node node, ast::Paragraph& paragraph) { + ast::Span span; + + if (const auto id_attr = node.attribute("xml:id")) { + const std::string_view id = id_attr.value(); + if (id.empty()) { + return make_error("xml:id must not be empty", node); + } + if (!register_id(id)) { + return make_error("duplicate xml:id", node); + } + span.id = id; + } + + if (const auto error = parse_timing(node, span.timing)) { + return error; + } + if (const auto error = parse_region_ref(node, span.region)) { + return error; + } + if (const auto error = parse_style_refs(node, span.style_refs)) { + return error; + } + + for (auto child = node.first_child(); !child.empty(); child = child.next_sibling()) { + switch (child.type()) { + case pugi::node_pcdata: + case pugi::node_cdata: + span.content.emplace_back(std::string(child.value())); + break; + case pugi::node_element: + if (std::string_view(child.name()) != "br") { + return make_error("span element may only contain text and br elements", child); + } + if (child.first_child()) { + return make_error("br element must be empty", child); + } + span.content.emplace_back(ast::LineBreak{}); + break; + default: + return make_error("span element may only contain text and br elements", child); + } + } + + paragraph.spans.push_back(std::move(span)); + + return {}; + } + + std::optional parse_head(pugi::xml_node head) { + for (auto child = head.first_child(); !child.empty(); child = child.next_sibling()) { + const std::string_view child_name = child.name(); + if (child_name == "styling") { + const auto error = parse_styling(child); + if (error) { + return error; + } + } + else if (child_name == "layout") { + const auto error = parse_layout(child); + if (error) { + return error; + } + } + } + + return {}; + } + + std::optional parse_styling(pugi::xml_node styling) { + for (auto child = styling.first_child(); !child.empty(); child = child.next_sibling()) { + const std::string_view child_name = child.name(); + if (child_name != "style") { + continue; + } + + const auto id_attr = child.attribute("xml:id"); + if (!id_attr) { + return make_error("style element requires xml:id", child); + } + + const std::string_view id = id_attr.value(); + if (id.empty()) { + return make_error("xml:id must not be empty", child); + } + + if (!register_id(id)) { + return make_error("duplicate xml:id", child); + } + + ast::StyleDefinition definition; + definition.id = id; + const auto refs_error = parse_style_refs(child, definition.style_refs); + if (refs_error) { + return refs_error; + } + + const auto error = parse_style_properties(child, definition.style, StyleScope::Content); + if (error) { + return error; + } + ast_doc_.styles.push_back(std::move(definition)); + } + + return {}; + } + + std::optional parse_style_properties(pugi::xml_node style, StyleProperties& output, StyleScope scope) { + for (auto attr = style.first_attribute(); !attr.empty(); attr = attr.next_attribute()) { + const std::string_view attr_name = attr.name(); + const std::string_view attr_value = attr.value(); + + if (attr_name == "tts:backgroundColor") { + const auto color = parse_color(attr_value); + if (!color) { + return make_error("invalid value for tts:backgroundcolor", style); + } + output.background_color = color; + } + else if (attr_name == "tts:color") { + const auto color = parse_color(attr_value); + if (!color) { + return make_error("invalid value for tts:color", style); + } + output.color = color; + } + else if (attr_name == "tts:extent" && scope == StyleScope::Region) { + const auto extent = parse_length_pair(attr_value, SingleLengthMode::Reject); + if (!extent || extent->x < 0.0 || extent->y < 0.0) { + return make_error("invalid value for tts:extent", style); + } + output.extent = extent; + } + else if (attr_name == "tts:fontFamily") { + if (attr_value.empty()) { + return make_error("invalid value for tts:fontfamily", style); + } + output.font_family = attr_value; + } + else if (attr_name == "tts:fontSize") { + const auto font_size = parse_length_pair(attr_value, SingleLengthMode::Repeat); + if (!font_size || + font_size->x < 16.0 || font_size->x > 144.0 || + font_size->y < 16.0 || font_size->y > 144.0) { + return make_error("invalid value for tts:fontsize", style); + } + output.font_size = font_size; + } + else if (attr_name == "tts:fontStyle") { + if (attr_value == "normal") { + output.font_style = StyleFontStyleValue::Normal; + } + else if (attr_value == "italic") { + output.font_style = StyleFontStyleValue::Italic; + } + else { + return make_error("invalid value for tts:fontstyle", style); + } + } + else if (attr_name == "tts:fontWeight") { + if (attr_value == "normal") { + output.font_weight = StyleFontWeightValue::Normal; + } + else if (attr_value == "bold") { + output.font_weight = StyleFontWeightValue::Bold; + } + else { + return make_error("invalid value for tts:fontweight", style); + } + } + else if (attr_name == "tts:lineHeight") { + if (attr_value == "normal") { + output.line_height = NormalLineHeight{}; + } + else { + const auto line_height = parse_length(attr_value); + if (!line_height || *line_height < 0.0) { + return make_error("invalid value for tts:lineheight", style); + } + output.line_height = *line_height; + } + } + else if (attr_name == "tts:origin" && scope == StyleScope::Region) { + const auto origin = parse_length_pair(attr_value, SingleLengthMode::Reject); + const auto is_in_range = [](double value) { + return value >= static_cast(std::numeric_limits::min()) && + value <= static_cast(std::numeric_limits::max()); + }; + if (!origin || !is_in_range(origin->x) || !is_in_range(origin->y)) { + return make_error("invalid value for tts:origin", style); + } + output.origin = origin; + } + else if (attr_name == "tts:textDecoration") { + if (attr_value == "none") { + output.text_decoration = StyleTextDecorationValue::None; + } + else if (attr_value == "underline") { + output.text_decoration = StyleTextDecorationValue::Underline; + } + else { + return make_error("invalid value for tts:textdecoration", style); + } + } + else if (attr_name == "tts:textOutline") { + const auto text_outline = parse_text_outline(attr_value); + if (!text_outline || + (std::holds_alternative(*text_outline) && + (std::get(*text_outline).border_width < 0.0 || + std::get(*text_outline).blur_width < 0.0))) { + return make_error("invalid value for tts:textoutline", style); + } + + output.text_outline = text_outline; + } + else if (attr_name == "tts:writingMode") { + if (attr_value == "lrtb") { + output.writing_mode = StyleWritingModeValue::Lrtb; + } + else if (attr_value == "tbrl") { + output.writing_mode = StyleWritingModeValue::Tbrl; + } + else { + return make_error("invalid value for tts:writingmode", style); + } + } + else if (attr_name == "arib-tt:letter-spacing") { + const auto letter_spacing = parse_length(attr_value); + if (!letter_spacing) { + return make_error("invalid value for arib-tt:letter-spacing", style); + } + if (*letter_spacing < 0) { + return make_error("invalid value for arib-tt:letter-spacing", style); + } + + output.letter_spacing = letter_spacing; + } + else if (attr_name == "arib-tt:ruby") { + if (attr_value.empty()) { + return make_error("invalid value for arib-tt:ruby", style); + } + + output.ruby = attr_value; + } + } + + return {}; + } + + std::optional parse_style_refs(pugi::xml_node node, std::vector& output) { + if (const auto style_attr = node.attribute("style")) { + const auto refs = split_by_whitespace(style_attr.value()); + if (refs.empty()) { + return make_error("style must not be empty", node); + } + for (const auto ref : refs) { + output.emplace_back(ref); + } + } + return {}; + } + + std::optional parse_region_ref(pugi::xml_node node, std::optional& output) { + const auto region_attr = node.attribute("region"); + if (!region_attr) { + return {}; + } + + const std::string_view region = region_attr.value(); + const auto refs = split_by_whitespace(region); + if (refs.size() != 1 || refs.front() != region) { + return make_error("region must contain exactly one region reference without surrounding whitespace", node); + } + + output = region; + return {}; + } + + std::optional parse_layout(pugi::xml_node layout) { + for (auto child = layout.first_child(); !child.empty(); child = child.next_sibling()) { + const std::string_view child_name = child.name(); + if (child_name != "region") { + return make_error("layout element may only contain region elements", child); + } + + const auto id_attr = child.attribute("xml:id"); + if (!id_attr) { + return make_error("region element requires xml:id", child); + } + + const std::string_view id = id_attr.value(); + if (id.empty()) { + return make_error("xml:id must not be empty", child); + } + + if (!register_id(id)) { + return make_error("duplicate xml:id", child); + } + + ast::RegionDefinition definition; + definition.id = id; + const auto refs_error = parse_style_refs(child, definition.style_refs); + if (refs_error) { + return refs_error; + } + const auto error = parse_style_properties(child, definition.style, StyleScope::Region); + if (error) { + return error; + } + ast_doc_.regions.push_back(std::move(definition)); + } + + return {}; + } + + std::optional parse_timing(pugi::xml_node node, ast::Timing& timing) { + if (mode_ == SyncMode::Async) { + return {}; + } + + if (const auto begin_attr = node.attribute("begin")) { + auto begin = parse_time_expression(begin_attr.value()); + if (!begin) { + return make_error("invalid value for begin", node); + } + + timing.begin = *begin; + } + + if (const auto end_attr = node.attribute("end")) { + auto end = parse_time_expression(end_attr.value()); + if (!end) { + return make_error("invalid value for end", node); + } + + timing.end = *end; + } + + return {}; + } + + bool register_id(std::string_view id) { + if (!ids_.emplace(id).second) { + return false; + } + + return true; + } + + ParseError make_error(std::string message) const { + return { + .location = {0, 1, 1}, + .message = std::move(message), + }; + } + + ParseError make_error(std::string message, const pugi::xml_node& node) const { + return { + .location = get_source_location(node, xml_), + .message = std::move(message), + }; + } + +private: + std::string xml_; + SyncMode mode_; + ast::Document ast_doc_; + std::unordered_set ids_; +}; + +} + +ParseResult parse(std::string_view xml, SyncMode mode) { + return Parser{ xml, mode }.run(); +} + +} + +} diff --git a/src/ttml/parser.h b/src/ttml/parser.h new file mode 100644 index 0000000..a5b5680 --- /dev/null +++ b/src/ttml/parser.h @@ -0,0 +1,39 @@ +#pragma once +#include "ttml/ast.h" +#include "ttml/sync_mode.h" + +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +struct SourceLocation { + size_t byte_offset{}; + uint32_t line{}; + uint32_t column{}; +}; + +struct ParseError { + SourceLocation location; + std::string message; +}; + +struct ParseResult { + std::optional document; + std::optional error; + + [[nodiscard]] bool has_error() const noexcept { + return error.has_value(); + } +}; + +[[nodiscard]] ParseResult parse(std::string_view xml, SyncMode mode); + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/resolved.h b/src/ttml/resolved.h new file mode 100644 index 0000000..4ac2ccd --- /dev/null +++ b/src/ttml/resolved.h @@ -0,0 +1,55 @@ +#pragma once + +#include "ttml/ast.h" + +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +namespace resolved { + +struct Region { + std::string id; + StyleProperties style; +}; + +struct Timing { + std::optional begin; + std::optional end; +}; + +struct Span { + std::optional id; + Timing timing; + StyleProperties style; + std::optional region; + std::vector content; +}; + +struct Paragraph { + std::optional id; + Timing timing; + StyleProperties style; + std::optional region; + std::vector spans; +}; + +struct Division { + Timing timing; + std::vector paragraphs; +}; + +struct Document { + std::optional division; +}; + +} // namespace arib: + +} // namespace ttml + +} // namespace resolved diff --git a/src/ttml/resolver.cpp b/src/ttml/resolver.cpp new file mode 100644 index 0000000..9e1d459 --- /dev/null +++ b/src/ttml/resolver.cpp @@ -0,0 +1,306 @@ +#include "ttml/resolver.h" + +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +namespace { + +template +void merge_property(std::optional& target, const std::optional& source) { + if (source) { + target = source; + } +} + +template +void apply_initial(std::optional& target, const std::optional& initial) { + if (!target) { + target = initial; + } +} + +void merge_style(StyleProperties& target, const StyleProperties& source) { + merge_property(target.background_color, source.background_color); + merge_property(target.color, source.color); + merge_property(target.extent, source.extent); + merge_property(target.font_family, source.font_family); + merge_property(target.font_size, source.font_size); + merge_property(target.font_style, source.font_style); + merge_property(target.font_weight, source.font_weight); + merge_property(target.line_height, source.line_height); + merge_property(target.origin, source.origin); + merge_property(target.text_decoration, source.text_decoration); + merge_property(target.text_outline, source.text_outline); + merge_property(target.writing_mode, source.writing_mode); + merge_property(target.letter_spacing, source.letter_spacing); + merge_property(target.ruby, source.ruby); +} + +StyleProperties inherited_style(const StyleProperties& parent) { + StyleProperties result; + result.background_color = parent.background_color; + result.color = parent.color; + result.font_family = parent.font_family; + result.font_size = parent.font_size; + result.font_style = parent.font_style; + result.font_weight = parent.font_weight; + result.line_height = parent.line_height; + result.text_decoration = parent.text_decoration; + result.text_outline = parent.text_outline; + result.writing_mode = parent.writing_mode; + result.letter_spacing = parent.letter_spacing; + result.ruby = parent.ruby; + return result; +} + +const StyleProperties& initial_style() { + static const StyleProperties style{ + .background_color = StyleColorValue{ { 0, 0, 0, 0 } }, + .color = StyleColorValue{ { 255, 255, 255, 255 } }, + .font_family = "round gothic", + .font_style = StyleFontStyleValue::Normal, + .font_weight = StyleFontWeightValue::Normal, + .line_height = NormalLineHeight{}, + .text_decoration = StyleTextDecorationValue::None, + .text_outline = None{}, + .writing_mode = StyleWritingModeValue::Lrtb, + .letter_spacing = 0.0, + }; + return style; +} + +void complete_style(StyleProperties& style) { + const auto& initial = initial_style(); + apply_initial(style.background_color, initial.background_color); + apply_initial(style.color, initial.color); + apply_initial(style.font_family, initial.font_family); + apply_initial(style.font_size, initial.font_size); + apply_initial(style.font_style, initial.font_style); + apply_initial(style.font_weight, initial.font_weight); + apply_initial(style.line_height, initial.line_height); + apply_initial(style.text_decoration, initial.text_decoration); + apply_initial(style.text_outline, initial.text_outline); + apply_initial(style.writing_mode, initial.writing_mode); + apply_initial(style.letter_spacing, initial.letter_spacing); +} + +struct TimingResolution { + resolved::Timing timing; + bool has_error{false}; +}; + +TimingResolution resolve_timing(const ast::Timing& timing, const resolved::Timing& parent, SyncMode mode) { + if (mode == SyncMode::Async) { + return {}; + } + resolved::Timing resolved_timing; + resolved_timing.begin = timing.begin + ? std::optional{parent.begin.value_or(std::chrono::milliseconds{}) + *timing.begin} + : parent.begin; + resolved_timing.end = timing.end + ? std::optional{parent.begin.value_or(std::chrono::milliseconds{}) + *timing.end} + : parent.end; + + if (parent.end) { + if (resolved_timing.begin && *resolved_timing.begin >= *parent.end) { + return { + .timing = std::move(resolved_timing), + .has_error = true + }; + } + if (resolved_timing.end && *resolved_timing.end > *parent.end) { + resolved_timing.end = parent.end; + } + } + + const auto effective_begin = resolved_timing.begin.value_or(std::chrono::milliseconds{}); + if (resolved_timing.end && effective_begin >= *resolved_timing.end) { + return { + .timing = std::move(resolved_timing), + .has_error = true + }; + } + + return { + .timing = std::move(resolved_timing) + }; +} + +class Resolver { +public: + Resolver(const ast::Document& document, SyncMode mode) + : document_(document), mode_(mode) { + styles_.reserve(document.styles.size()); + regions_.reserve(document.regions.size()); + resolved_styles_.reserve(document.styles.size()); + resolving_styles_.reserve(document.styles.size()); + + for (const auto& style : document.styles) { + styles_.emplace(std::string_view{style.id}, &style); + } + for (const auto& region : document.regions) { + regions_.emplace(std::string_view{region.id}, ®ion); + } + } + + ResolveResult run() { + resolved::Document output; + if (!document_.division) { + return { + .document = std::move(output) + }; + } + + const auto& division = *document_.division; + auto division_timing = resolve_timing(division.timing, {}, mode_); + if (division_timing.has_error) { + return { + .document = std::move(output) + }; + } + + resolved::Division resolved_division; + resolved_division.timing = std::move(division_timing.timing); + resolved_division.paragraphs.reserve(division.paragraphs.size()); + + for (const auto& paragraph : division.paragraphs) { + auto paragraph_timing = resolve_timing(paragraph.timing, resolved_division.timing, mode_); + if (paragraph_timing.has_error) { + continue; + } + + resolved::Paragraph resolved_paragraph; + resolved_paragraph.id = paragraph.id; + resolved_paragraph.timing = std::move(paragraph_timing.timing); + resolved_paragraph.spans.reserve(paragraph.spans.size()); + + if (auto error = resolve_region(paragraph.region, resolved_paragraph.region)) { + return { + .document = std::nullopt, + .error = std::move(error) + }; + } + + if (resolved_paragraph.region) { + resolved_paragraph.style = inherited_style(resolved_paragraph.region->style); + } + + for (const auto& span : paragraph.spans) { + auto span_timing = resolve_timing(span.timing, resolved_paragraph.timing, mode_); + if (span_timing.has_error) { + continue; + } + + resolved::Span resolved_span; + resolved_span.id = span.id; + resolved_span.timing = std::move(span_timing.timing); + resolved_span.style = inherited_style(resolved_paragraph.style); + if (auto error = resolve_region(span.region, resolved_span.region)) { + return { + .document = {}, + .error = std::move(error) + }; + } + + if (resolved_span.region) { + merge_style(resolved_span.style, inherited_style(resolved_span.region->style)); + } + if (auto error = apply_refs(resolved_span.style, span.style_refs)) { + return { + .document = {}, + .error = std::move(error) + }; + } + complete_style(resolved_span.style); + resolved_span.content = span.content; + resolved_paragraph.spans.push_back(std::move(resolved_span)); + } + + resolved_division.paragraphs.push_back(std::move(resolved_paragraph)); + } + + output.division = std::move(resolved_division); + return { + .document = std::move(output), + .error = {} + }; + } + +private: + std::optional apply_refs(StyleProperties& target, const std::vector& refs) { + for (const auto& ref : refs) { + StyleProperties resolved; + if (auto error = resolve_style(ref, resolved)) { + return error; + } + + merge_style(target, resolved); + } + return {}; + } + + std::optional resolve_style(std::string_view id, StyleProperties& output) { + if (const auto cached = resolved_styles_.find(id); cached != resolved_styles_.end()) { + output = cached->second; + return {}; + } + const auto found = styles_.find(id); + if (found == styles_.end()) { + return ResolveError{ "unknown style reference: " + std::string{id} }; + } + if (!resolving_styles_.emplace(id).second) { + return ResolveError{ "cyclic style reference: " + std::string{id} }; + } + + StyleProperties resolved; + if (auto error = apply_refs(resolved, found->second->style_refs)) { + resolving_styles_.erase(id); + return error; + } + merge_style(resolved, found->second->style); + resolving_styles_.erase(id); + resolved_styles_.emplace(id, resolved); + output = std::move(resolved); + return {}; + } + + std::optional resolve_region( + const std::optional& id, + std::optional& output) { + if (!id) return {}; + const auto found = regions_.find(*id); + if (found == regions_.end()) { + return ResolveError{ "unknown region reference: " + *id }; + } + StyleProperties style; + if (auto error = apply_refs(style, found->second->style_refs)) { + return error; + } + merge_style(style, found->second->style); + output = resolved::Region{ found->second->id, std::move(style) }; + return {}; + } + + const ast::Document& document_; + SyncMode mode_; + std::unordered_map styles_; + std::unordered_map regions_; + std::unordered_map resolved_styles_; + std::unordered_set resolving_styles_; +}; + +} // namespace + +ResolveResult resolve(const ast::Document& document, SyncMode mode) { + return Resolver{document, mode}.run(); +} + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/resolver.h b/src/ttml/resolver.h new file mode 100644 index 0000000..85845e0 --- /dev/null +++ b/src/ttml/resolver.h @@ -0,0 +1,31 @@ +#pragma once + +#include "ttml/ast.h" +#include "ttml/resolved.h" +#include "ttml/sync_mode.h" + +#include +#include + +namespace arib { + +namespace ttml { + +struct ResolveError { + std::string message; +}; + +struct ResolveResult { + std::optional document; + std::optional error; + + [[nodiscard]] bool has_error() const noexcept { + return error.has_value(); + } +}; + +[[nodiscard]] ResolveResult resolve(const ast::Document& document, SyncMode mode); + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/string_utils.h b/src/ttml/string_utils.h new file mode 100644 index 0000000..a6fab5a --- /dev/null +++ b/src/ttml/string_utils.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +constexpr std::string_view kWhitespace = " \t\r\n"; + +[[nodiscard]] inline std::optional string_to_double(std::string_view value) { + if (value.empty()) { + return {}; + } + + double result{}; + const char* const begin = value.data(); + const char* const end = begin + value.size(); + const auto [parsed_end, error] = std::from_chars(begin, end, result); + + if (error != std::errc{} || parsed_end != end || !std::isfinite(result)) { + return {}; + } + + return result; +} + +[[nodiscard]] inline std::vector split_by_whitespace(std::string_view value) { + std::vector result; + auto start = value.find_first_not_of(kWhitespace); + + while (start != std::string_view::npos) { + const auto end = value.find_first_of(kWhitespace, start); + if (end == std::string_view::npos) { + result.emplace_back(value.substr(start)); + break; + } + + result.emplace_back(value.substr(start, end - start)); + start = value.find_first_not_of(kWhitespace, end); + } + + return result; +} + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/style.cpp b/src/ttml/style.cpp new file mode 100644 index 0000000..be15c92 --- /dev/null +++ b/src/ttml/style.cpp @@ -0,0 +1,166 @@ +#include "ttml/style.h" +#include "ttml/string_utils.h" +#include +#include + +namespace arib { + +namespace ttml { + +namespace { + +bool hex_to_bytes(std::string_view input, std::span output) { + if (input.size() % 2 != 0 || input.size() / 2 > output.size()) { + return false; + } + + auto hex = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; + + const auto byte_count = input.size() / 2; + for (size_t i = 0; i < byte_count; ++i) { + const int hi = hex(input[i * 2]); + const int lo = hex(input[i * 2 + 1]); + + if (hi < 0 || lo < 0) { + return false; + } + + output[i] = static_cast((hi << 4) | lo); + } + + return true; +} + +} + +std::optional parse_color(std::string_view color_string) { + if (color_string.empty()) { + return {}; + } + + if (color_string[0] != '#') { + return {}; + } + + StyleColorValue color; + if (color_string.length() == 7) { + // #RRGGBB + if (!hex_to_bytes(color_string.substr(1), color.rgba)) { + return {}; + } + + color.rgba[3] = 0xFF; + } + else if (color_string.length() == 9) { + // #RRGGBBAA + if (!hex_to_bytes(color_string.substr(1), color.rgba)) { + return {}; + } + } + else { + return {}; + } + + return color; +} + +std::optional parse_length(std::string_view value) { + constexpr std::string_view suffix = "px"; + if (!value.ends_with(suffix)) { + return {}; + } + + value.remove_suffix(suffix.size()); + return string_to_double(value); +} + +std::optional parse_length_pair(std::string_view length_string, SingleLengthMode single_length_mode) { + if (length_string.empty()) { + return {}; + } + + if (kWhitespace.find(length_string.front()) != std::string_view::npos || + kWhitespace.find(length_string.back()) != std::string_view::npos) { + return {}; + } + + const auto split = split_by_whitespace(length_string); + if (split.empty() || split.size() > 2) { + return {}; + } + + const auto x = parse_length(split[0]); + if (!x) { + return {}; + } + + if (split.size() == 1) { + if (single_length_mode == SingleLengthMode::Reject) { + return {}; + } + return LengthPair{ *x, *x }; + } + + const auto y = parse_length(split[1]); + if (!y) { + return {}; + } + + return LengthPair{ *x, *y }; +} + +std::optional parse_text_outline(std::string_view value_string) { + if (value_string.empty()) { + return {}; + } + + if (value_string == "none") { + return None{}; + } + + if (kWhitespace.find(value_string.front()) != std::string_view::npos || + kWhitespace.find(value_string.back()) != std::string_view::npos) { + return {}; + } + + const auto split = split_by_whitespace(value_string); + if (split.empty() || split.size() > 3) { + return {}; + } + + TextOutline text_outline; + size_t length_index = 0; + if (const auto color = parse_color(split.front())) { + text_outline.color = *color; + length_index = 1; + } + + const auto length_count = split.size() - length_index; + if (length_count == 0 || length_count > 2) { + return {}; + } + + const auto border_width = parse_length(split[length_index]); + if (!border_width) { + return {}; + } + text_outline.border_width = *border_width; + + if (length_count == 2) { + const auto blur_width = parse_length(split[length_index + 1]); + if (!blur_width) { + return {}; + } + text_outline.blur_width = *blur_width; + } + + return text_outline; +} +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/style.h b/src/ttml/style.h new file mode 100644 index 0000000..9989a44 --- /dev/null +++ b/src/ttml/style.h @@ -0,0 +1,91 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +struct None {}; + +struct StyleColorValue { + std::array rgba; + + constexpr uint8_t r() const { return rgba[0]; } + constexpr uint8_t g() const { return rgba[1]; } + constexpr uint8_t b() const { return rgba[2]; } + constexpr uint8_t a() const { return rgba[3]; } + + friend constexpr bool operator==(const StyleColorValue&, const StyleColorValue&) = default; +}; + +struct LengthPair { + double x{}; + double y{}; +}; + +enum class SingleLengthMode { + Reject, + Repeat, +}; + +enum class StyleFontStyleValue { + Normal, + Italic +}; + +enum class StyleFontWeightValue { + Normal, + Bold +}; + +struct NormalLineHeight {}; +using StyleLineHeightValue = std::variant; + +enum class StyleTextDecorationValue { + None, + Underline +}; + +struct TextOutline { + std::optional color; + double border_width{}; + double blur_width{}; +}; + +using StyleTextOutlineValue = std::variant; + +enum class StyleWritingModeValue { + Lrtb, + Tbrl +}; + +struct StyleProperties { + std::optional background_color; + std::optional color; + std::optional extent; + std::optional font_family; + std::optional font_size; + std::optional font_style; + std::optional font_weight; + std::optional line_height; + std::optional origin; + std::optional text_decoration; + std::optional text_outline; + std::optional writing_mode; + std::optional letter_spacing; + std::optional ruby; +}; + +[[nodiscard]] std::optional parse_color(std::string_view text); +[[nodiscard]] std::optional parse_length(std::string_view value); +[[nodiscard]] std::optional parse_length_pair(std::string_view length_string, SingleLengthMode single_length_mode); +[[nodiscard]] std::optional parse_text_outline(std::string_view value_string); + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/sync_mode.h b/src/ttml/sync_mode.h new file mode 100644 index 0000000..70117bf --- /dev/null +++ b/src/ttml/sync_mode.h @@ -0,0 +1,14 @@ +#pragma once + +namespace arib { + +namespace ttml { + +enum class SyncMode { + Sync, + Async, +}; + +} // namespace ttml + +} // namespace arib