From 4b710261709bb175d2018edc5cf4cc1f935212f2 Mon Sep 17 00:00:00 2001 From: nekohkr <182421472+nekohkr@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:02:10 +0900 Subject: [PATCH 01/11] Fix reading aggregated signaling messages from wrong stream --- src/mmtTlvDemuxer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mmtTlvDemuxer.cpp b/src/mmtTlvDemuxer.cpp index 94f3948..f4d941b 100644 --- a/src/mmtTlvDemuxer.cpp +++ b/src/mmtTlvDemuxer.cpp @@ -864,7 +864,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); From 91b048740ca9db803f509076636053f5ce8c7702 Mon Sep 17 00:00:00 2001 From: nekohkr <182421472+nekohkr@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:09:31 +0900 Subject: [PATCH 02/11] Rewrite TTML parser and B24 converter --- msvc/dantto4k.vcxproj | 89 ++- msvc/dantto4k.vcxproj.filters | 50 +- msvc/dantto4k_dll.vcxproj | 76 +- src/b24Color.cpp | 8 +- src/b24ControlSet.h | 7 +- src/b24SubtitleConverter.cpp | 131 ++++ ...itleConvertor.h => b24SubtitleConverter.h} | 6 +- src/b24SubtitleConvertor.cpp | 269 ------- src/dantto4k.cpp | 122 +-- src/remuxerHandler.cpp | 167 ++++- src/remuxerHandler.h | 51 +- src/ttml.cpp | 372 ++++++--- src/ttml.h | 130 ++-- src/ttml/ast.h | 68 ++ src/ttml/b24_converter.cpp | 709 ++++++++++++++++++ src/ttml/b24_converter.h | 39 + src/ttml/parser.cpp | 663 ++++++++++++++++ src/ttml/parser.h | 33 + src/ttml/resolved.h | 55 ++ src/ttml/resolver.cpp | 232 ++++++ src/ttml/resolver.h | 26 + src/ttml/string_utils.h | 28 + src/ttml/style.cpp | 208 +++++ src/ttml/style.h | 89 +++ 24 files changed, 3002 insertions(+), 626 deletions(-) create mode 100644 src/b24SubtitleConverter.cpp rename src/{b24SubtitleConvertor.h => b24SubtitleConverter.h} (96%) delete mode 100644 src/b24SubtitleConvertor.cpp create mode 100644 src/ttml/ast.h create mode 100644 src/ttml/b24_converter.cpp create mode 100644 src/ttml/b24_converter.h create mode 100644 src/ttml/parser.cpp create mode 100644 src/ttml/parser.h create mode 100644 src/ttml/resolved.h create mode 100644 src/ttml/resolver.cpp create mode 100644 src/ttml/resolver.h create mode 100644 src/ttml/string_utils.h create mode 100644 src/ttml/style.cpp create mode 100644 src/ttml/style.h diff --git a/msvc/dantto4k.vcxproj b/msvc/dantto4k.vcxproj index 944df95..7edefce 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 + MultiThreadedDLL + /FS %(AdditionalOptions) + $(ProjectDir)../src;%(AdditionalIncludeDirectories) + Console true true true - %(AdditionalDependencies) - - + %(AdditionalDependencies) + + @@ -165,7 +167,7 @@ - + @@ -248,6 +250,10 @@ + + + + @@ -271,7 +277,7 @@ - + @@ -372,8 +378,15 @@ + + + + + + + - + \ No newline at end of file diff --git a/msvc/dantto4k.vcxproj.filters b/msvc/dantto4k.vcxproj.filters index 6cae4f5..9133df2 100644 --- a/msvc/dantto4k.vcxproj.filters +++ b/msvc/dantto4k.vcxproj.filters @@ -244,7 +244,7 @@ dantto4k\xml - + dantto4k\subtitle @@ -304,12 +304,24 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + @@ -594,7 +606,7 @@ dantto4k\xml - + dantto4k\subtitle @@ -672,12 +684,33 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + @@ -734,5 +767,8 @@ {5dccb4d7-560f-4244-9d84-334de3cf19f2} + + {84aaa3a8-9e4a-4085-9dc5-db67d16195cc} + - + \ No newline at end of file diff --git a/msvc/dantto4k_dll.vcxproj b/msvc/dantto4k_dll.vcxproj index 1a75873..7566d5f 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,40 @@ 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) + 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 + MultiThreadedDLL + /FS %(AdditionalOptions) + Windows true true true false - %(AdditionalDependencies) - - + %(AdditionalDependencies) + + @@ -175,7 +175,7 @@ - + @@ -262,7 +262,7 @@ - + @@ -280,7 +280,7 @@ - + @@ -384,11 +384,11 @@ - + - + \ No newline at end of file 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..80574a6 --- /dev/null +++ b/src/b24SubtitleConverter.cpp @@ -0,0 +1,131 @@ +#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 + +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()); +} + +void appendCharacterComposition(std::vector& output, uint32_t width, uint32_t height) { + output.push_back(B24ControlSet::CSI); + appendNumber(output, width); + output.push_back(0x3B); + appendNumber(output, height); + output.push_back(B24ControlSet::SP); + output.push_back(B24ControlSet::SSM); +} + +void appendSpacing(std::vector& output, uint32_t spacing, uint8_t control) { + output.push_back(B24ControlSet::CSI); + appendNumber(output, spacing); + output.push_back(B24ControlSet::SP); + output.push_back(control); +} + +void appendTwoDigits(std::vector& output, uint8_t value) { + output.push_back(static_cast(0x30 + value / 10)); + output.push_back(static_cast(0x30 + value % 10)); +} + +void appendOrnament(std::vector& output, uint8_t palette, uint8_t index) { + output.push_back(B24ControlSet::CSI); + output.push_back(0x31); + output.push_back(0x3B); + appendTwoDigits(output, palette); + appendTwoDigits(output, index); + output.push_back(B24ControlSet::SP); + output.push_back(B24ControlSet::ORN); +} + +void appendOrnamentOff(std::vector& output) { + output.push_back(B24ControlSet::CSI); + output.push_back(0x30); + output.push_back(B24ControlSet::SP); + output.push_back(B24ControlSet::ORN); +} + +} + +bool B24SubtitleConverter::convert(const std::string& input, std::list& output, B24::PESData::PESType pesType) { + std::string_view ttml_input = input; + + auto parse_result = arib::ttml::parse(ttml_input); + if (parse_result.error) { + std::cerr << "[TTML] " << parse_result.error->message << std::endl; + return false; + } + auto resolve_result = arib::ttml::resolve(*parse_result.document); + if (resolve_result.error) { + std::cerr << "[TTML] " << resolve_result.error->message << std::endl; + return false; + } + auto convert_result = arib::ttml::convert_to_b24(*resolve_result.document, + pesType == B24::PESData::PESType::Synchronized + ? arib::ttml::PESType::Sync + : arib::ttml::PESType::Async); + if (convert_result.error) { + std::cerr << "[TTML] " << convert_result.error->message << std::endl; + 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 96% rename from src/b24SubtitleConvertor.h rename to src/b24SubtitleConverter.h index 826390a..fa35851 100644 --- a/src/b24SubtitleConvertor.h +++ b/src/b24SubtitleConverter.h @@ -313,8 +313,8 @@ class B24SubtitleOutput { std::optional begin; }; -class B24SubtitleConvertor { +class B24SubtitleConverter { public: - static bool convert(const std::string& input, std::list& output); + static bool convert(const std::string& input, std::list& output, B24::PESData::PESType pesType); -}; \ No newline at end of file +}; 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 18942dc..aeaab57 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; @@ -160,25 +160,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 { @@ -253,30 +253,30 @@ int main(int argc, char* argv[]) { return 1; } - 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; @@ -284,14 +284,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/remuxerHandler.cpp b/src/remuxerHandler.cpp index 797aac1..842bd5f 100644 --- a/src/remuxerHandler.cpp +++ b/src/remuxerHandler.cpp @@ -36,12 +36,18 @@ #include "mmtTlvDemuxer.h" #include "timebase.h" #include "config.h" -#include "ntp.h" -#include "b24SubtitleConvertor.h" - -namespace { - -int convertRunningStatus(int runningStatus) { +#include "ntp.h" +#include "b24SubtitleConverter.h" +#include +#include +#include + +namespace { + +constexpr uint64_t SUBTITLE_PRELOAD_TIME = 90 * 1000; +constexpr uint64_t SUBTITLE_MINIMUM_FUTURE_TIME = 90 * 200; + +int convertRunningStatus(int runningStatus) { switch (runningStatus) { case 0: return 0; @@ -140,19 +146,55 @@ void RemuxerHandler::onAudioData(const MmtTlv::MmtStream& mmtStream, const MmtTl writeStream(mmtStream, mfuData, output); } -void RemuxerHandler::onSubtitleData(const MmtTlv::MmtStream& mmtStream, const struct MmtTlv::MfuData& mfuData) { - std::string ttml(mfuData.data.begin(), mfuData.data.end()); - std::list output; - B24SubtitleConvertor::convert(ttml, output); - - if (output.empty()) { - return; - } - - for (const auto& pesData : output) { - writeSubtitle(mmtStream, pesData); - } -} +void RemuxerHandler::onSubtitleData(const MmtTlv::MmtStream& mmtStream, const struct MmtTlv::MfuData& mfuData) { + std::string ttml(mfuData.data.begin(), mfuData.data.end()); + std::list output; + const auto pesType = mmtStream.getComponentTag() == 0x30 + ? B24::PESData::PESType::Synchronized + : B24::PESData::PESType::Asynchronous; + B24SubtitleConverter::convert(ttml, output, pesType); + + if (output.empty()) { + return; + } + + if (mmtStream.getComponentTag() != 0x30) { + for (const auto& pesData : output) { + writeSubtitle(mmtStream, pesData, 0); + } + return; + } + + const auto currentPts = lastPcr / 300; + const auto firstTimedOutput = std::find_if(output.begin(), output.end(), + [](const B24SubtitleOutput& subtitle) { + return subtitle.begin.has_value(); + }); + if (!subtitlePtsBase && firstTimedOutput != output.end()) { + subtitlePtsBase = programStartTime * 90000; + + if (currentPts != 0) { + const auto firstPts = *subtitlePtsBase + *firstTimedOutput->begin * 90; + const auto minimumPts = currentPts + SUBTITLE_MINIMUM_FUTURE_TIME; + if (firstPts < minimumPts) { + *subtitlePtsBase += minimumPts - firstPts; + } + } + } + + for (auto& pesData : output) { + const auto pts = pesData.begin + ? subtitlePtsBase.value_or(programStartTime * 90000) + *pesData.begin * 90 + : currentPts == 0 + ? 0 + : currentPts + SUBTITLE_MINIMUM_FUTURE_TIME; + queueSubtitle(mmtStream, std::move(pesData), pts); + } + + if (currentPts != 0) { + writePendingSubtitles(currentPts); + } +} void RemuxerHandler::onPacketDrop(uint16_t packetId, const MmtTlv::MmtStream* mmtStream) { if (mmtStream) { @@ -284,20 +326,48 @@ void RemuxerHandler::writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTl } } -void RemuxerHandler::writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle) { - 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; +void RemuxerHandler::queueSubtitle(const MmtTlv::MmtStream& mmtStream, B24SubtitleOutput subtitle, uint64_t pts) { + const auto position = std::upper_bound( + pendingSubtitles.begin(), pendingSubtitles.end(), pts, + [](uint64_t lhs, const PendingSubtitle& rhs) { + return lhs < rhs.pts; + }); + pendingSubtitles.insert(position, PendingSubtitle{ + .packetId = mmtStream.getPacketId(), + .pts = pts, + .subtitle = std::move(subtitle), + }); +} + +void RemuxerHandler::writePendingSubtitles(uint64_t pts) { + const auto preloadPts = pts + SUBTITLE_PRELOAD_TIME; + while (!pendingSubtitles.empty() && pendingSubtitles.front().pts <= preloadPts) { + auto pending = std::move(pendingSubtitles.front()); + pendingSubtitles.pop_front(); + + const auto* stream = demuxer.getStream(pending.packetId); + if (!stream) { + continue; + } + + writeSubtitle(*stream, pending.subtitle, pending.pts); + } +} + +void RemuxerHandler::writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, uint64_t pts) { + std::vector pesOutput; + + PESPacket pes; + if (mmtStream.getComponentTag() == 0x30) { + if (pts == 0) { + return; } pes.setPts(pts); } - pes.setStreamId(componentTagToStreamId(mmtStream.getComponentTag())); + pes.setStreamId(mmtStream.getComponentTag() == 0x30 + ? STREAM_ID_PRIVATE_STREAM_1 + : STREAM_ID_PRIVATE_STREAM_2); pes.setPayload(&subtitle.pesData); pes.setPayloadLength(subtitle.pesData.size()); if (mmtStream.getComponentTag() == 0x30) { @@ -388,7 +458,9 @@ void RemuxerHandler::writeCaptionManagementData(uint64_t pts) { if (stream.second.getComponentTag() == 0x30) { pes.setPts(lastCaptionManagementDataPts); } - pes.setStreamId(componentTagToStreamId(stream.second.getComponentTag())); + pes.setStreamId(stream.second.getComponentTag() == 0x30 + ? STREAM_ID_PRIVATE_STREAM_1 + : STREAM_ID_PRIVATE_STREAM_2); pes.setPayload(&packedPesData); pes.setPayloadLength(packedPesData.size()); if (stream.second.getComponentTag() == 0x30) { @@ -1071,8 +1143,9 @@ void RemuxerHandler::onNit(const MmtTlv::Nit& nit) { } } -void RemuxerHandler::onNtp(const MmtTlv::NTPv4& ntp) { - auto& cc = mapCC[PCR_PID]; +void RemuxerHandler::onNtp(const MmtTlv::NTPv4& ntp) { + const bool firstPcr = lastPcr == 0; + auto& cc = mapCC[PCR_PID]; ts::TSPacket packet; packet.init(PCR_PID, cc & 0xF, 0); cc++; @@ -1085,17 +1158,33 @@ void RemuxerHandler::onNtp(const MmtTlv::NTPv4& ntp) { lastPcr = ntp.transmit_timestamp.toPcrValue(); - writeCaptionManagementData(ntp.transmit_timestamp.toPcrValue() / 300); -} + const auto pts = ntp.transmit_timestamp.toPcrValue() / 300; + if (firstPcr && !pendingSubtitles.empty()) { + const auto minimumPts = pts + SUBTITLE_MINIMUM_FUTURE_TIME; + if (pendingSubtitles.front().pts < minimumPts) { + const auto ptsOffset = minimumPts - pendingSubtitles.front().pts; + for (auto& pending : pendingSubtitles) { + pending.pts += ptsOffset; + } + if (subtitlePtsBase) { + *subtitlePtsBase += ptsOffset; + } + } + } + writeCaptionManagementData(pts); + writePendingSubtitles(pts); +} void RemuxerHandler::clear() { mapService2Pid.clear(); mapCC.clear(); mapPesPendingData.clear(); - mapPesPacketIndex.clear(); - mapPesState.clear(); + mapPesPacketIndex.clear(); + mapPesState.clear(); + pendingSubtitles.clear(); tsid = -1; lastPcr = 0; - lastCaptionManagementDataPts = 0; - programStartTime = 0; -} + lastCaptionManagementDataPts = 0; + programStartTime = 0; + subtitlePtsBase.reset(); +} diff --git a/src/remuxerHandler.h b/src/remuxerHandler.h index c25be3f..43ccc63 100644 --- a/src/remuxerHandler.h +++ b/src/remuxerHandler.h @@ -1,10 +1,12 @@ #pragma once #include "demuxerHandler.h" -#include "b24SubtitleConvertor.h" -#include "damt.h" -#include -#include -#include +#include "b24SubtitleConverter.h" +#include "damt.h" +#include +#include +#include +#include +#include namespace StreamType { @@ -78,26 +80,35 @@ 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, + }; + struct PendingSubtitle { + uint16_t packetId; + uint64_t pts; + B24SubtitleOutput subtitle; + }; + + void writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTlv::MfuData& mfuData, const std::vector& data); + void queueSubtitle(const MmtTlv::MmtStream& mmtStream, B24SubtitleOutput subtitle, uint64_t pts); + void writePendingSubtitles(uint64_t pts); + void writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, uint64_t pts); + 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; + std::unordered_map mapPesPacketIndex; + std::unordered_map mapPesState; + std::list pendingSubtitles; 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, }; + uint64_t lastPcr{}; + uint64_t lastCaptionManagementDataPts{}; + uint64_t programStartTime{}; + std::optional subtitlePtsBase; + 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 index 5bdb1e2..e2b514d 100644 --- a/src/ttml.cpp +++ b/src/ttml.cpp @@ -3,7 +3,7 @@ namespace { -uint64_t parseTimestamp(const std::string& timestamp) { +uint64_t parseTimestamp(const std::string& timestamp) { size_t dotPos = timestamp.find('.'); uint64_t hours, minutes, seconds, millis = 0; @@ -40,10 +40,178 @@ uint64_t parseTimestamp(const std::string& timestamp) { seconds = std::stoull(timestamp.substr(6, 2)); } - return hours * 3600 * 1000ULL + minutes * 60 * 1000ULL + seconds * 1000ULL + millis; -} - -} + return hours * 3600 * 1000ULL + minutes * 60 * 1000ULL + seconds * 1000ULL + millis; +} + +void mergeStyle(TTMLStyle& target, const TTMLStyle& source) { + if (source.fontSize) target.fontSize = source.fontSize; + if (source.lineHeight) target.lineHeight = source.lineHeight; + if (source.letterSpacing) target.letterSpacing = source.letterSpacing; + if (source.fontWeight) target.fontWeight = source.fontWeight; + if (source.fontStyle) target.fontStyle = source.fontStyle; + if (source.color) target.color = source.color; + if (source.backgroundColor) target.backgroundColor = source.backgroundColor; + if (source.outlineSpecified) { + target.outlineSpecified = true; + target.outlineColor = source.outlineColor; + } + if (source.fontFamily) target.fontFamily = source.fontFamily; + if (source.textDecoration) target.textDecoration = source.textDecoration; + if (source.writingMode) target.writingMode = source.writingMode; + if (source.opacity) target.opacity = source.opacity; + if (source.border) target.border = source.border; + if (source.animation) target.animation = source.animation; + if (source.ruby) target.ruby = source.ruby; + if (source.textShadow) target.textShadow = source.textShadow; + if (source.overflow) target.overflow = source.overflow; +} + +TTMLStyle inheritedStyle(const TTMLStyle& parent) { + TTMLStyle result; + result.fontSize = parent.fontSize; + result.lineHeight = parent.lineHeight; + result.letterSpacing = parent.letterSpacing; + result.fontWeight = parent.fontWeight; + result.fontStyle = parent.fontStyle; + result.color = parent.color; + result.outlineSpecified = parent.outlineSpecified; + result.outlineColor = parent.outlineColor; + result.fontFamily = parent.fontFamily; + result.textDecoration = parent.textDecoration; + result.writingMode = parent.writingMode; + result.ruby = parent.ruby; + result.textShadow = parent.textShadow; + // tts:backgroundColor is not inherited in TTML. + return result; +} + +TTMLStyle initialStyle() { + TTMLStyle result; + // TR-B39 has no initial fontSize. Keep it absent so callers can + // distinguish an invalid/incomplete document from a 144px style. + result.lineHeight = TtmlStyleValue(TtmlStyleValueKeyword("normal")); + result.letterSpacing = TtmlStyleValue(TtmlStyleValueLength(0.0f, "px")); + result.fontWeight = TtmlStyleValue(TtmlStyleValueKeyword("normal")); + result.fontStyle = TtmlStyleValue(TtmlStyleValueKeyword("normal")); + result.color = TtmlStyleValue(TtmlStyleValueColor(255, 255, 255, 255)); + result.backgroundColor = TtmlStyleValue(TtmlStyleValueColor(0, 0, 0, 0)); + result.outlineSpecified = true; + result.fontFamily = "round gothic"; + result.textDecoration = TtmlStyleValue(TtmlStyleValueKeyword("none")); + result.writingMode = TtmlStyleValue(TtmlStyleValueKeyword("lrtb")); + result.opacity = TtmlStyleValue(TtmlStyleValueNumber(1.0f)); + result.border = "none 0px #00000000"; + result.animation = TtmlStyleValue(TtmlStyleValueKeyword("none")); + result.ruby = TtmlStyleValue(TtmlStyleValueKeyword("none")); + result.textShadow = "none"; + result.overflow = TtmlStyleValue(TtmlStyleValueKeyword("hidden")); + // textOutline initial value is "none", represented by no color. + return result; +} + +void completeComputedStyle(TTMLStyle& style) { + const TTMLStyle defaults = initialStyle(); + if (!style.lineHeight) style.lineHeight = defaults.lineHeight; + if (!style.letterSpacing) style.letterSpacing = defaults.letterSpacing; + if (!style.fontWeight) style.fontWeight = defaults.fontWeight; + if (!style.fontStyle) style.fontStyle = defaults.fontStyle; + if (!style.color) style.color = defaults.color; + if (!style.backgroundColor) style.backgroundColor = defaults.backgroundColor; + if (!style.fontFamily) style.fontFamily = defaults.fontFamily; + if (!style.textDecoration) style.textDecoration = defaults.textDecoration; + if (!style.writingMode) style.writingMode = defaults.writingMode; + if (!style.opacity) style.opacity = defaults.opacity; + if (!style.border) style.border = defaults.border; + if (!style.animation) style.animation = defaults.animation; + if (!style.ruby) style.ruby = defaults.ruby; + if (!style.textShadow) style.textShadow = defaults.textShadow; + if (!style.overflow) style.overflow = defaults.overflow; +} + +void applyTextOutline(const pugi::xml_attribute& attribute, TTMLStyle& target) { + if (!attribute) { + return; + } + + target.outlineSpecified = true; + target.outlineColor.reset(); + std::istringstream valueStream(attribute.value()); + std::string color; + valueStream >> color; + if (color == "none") { + return; + } + if (!color.empty() && color.front() == '#') { + target.outlineColor = TtmlStyleValueParser::parse(color); + } +} + +void applyReferencedStyles(const pugi::xml_node& node, const std::list& styles, TTMLStyle& target) { + std::istringstream styleStream(node.attribute("style").value()); + std::string styleId; + while (styleStream >> styleId) { + const auto style = std::find_if(styles.begin(), styles.end(), [&styleId](const TTMLStyle& candidate) { + return candidate.id == styleId; + }); + if (style != styles.end()) { + mergeStyle(target, *style); + } + } +} + +void applyInlineStyles(const pugi::xml_node& node, TTMLStyle& target) { + if (node.attribute("tts:fontSize")) { + target.fontSize = TtmlStyleValueParser::parseFontSize(node.attribute("tts:fontSize").value()); + } + if (node.attribute("tts:lineHeight")) { + target.lineHeight = TtmlStyleValueParser::parse(node.attribute("tts:lineHeight").value()); + } + if (node.attribute("arib-tt:letter-spacing")) { + target.letterSpacing = TtmlStyleValueParser::parse(node.attribute("arib-tt:letter-spacing").value()); + } + if (node.attribute("tts:fontWeight")) { + target.fontWeight = TtmlStyleValueParser::parse(node.attribute("tts:fontWeight").value()); + } + if (node.attribute("tts:fontStyle")) { + target.fontStyle = TtmlStyleValueParser::parse(node.attribute("tts:fontStyle").value()); + } + if (node.attribute("tts:color")) { + target.color = TtmlStyleValueParser::parse(node.attribute("tts:color").value()); + } + if (node.attribute("tts:backgroundColor")) { + target.backgroundColor = TtmlStyleValueParser::parse(node.attribute("tts:backgroundColor").value()); + } + if (node.attribute("tts:fontFamily")) { + target.fontFamily = node.attribute("tts:fontFamily").value(); + } + if (node.attribute("tts:textDecoration")) { + target.textDecoration = TtmlStyleValueParser::parse(node.attribute("tts:textDecoration").value()); + } + if (node.attribute("tts:writingMode")) { + target.writingMode = TtmlStyleValueParser::parse(node.attribute("tts:writingMode").value()); + } + if (node.attribute("tts:opacity")) { + target.opacity = TtmlStyleValueParser::parse(node.attribute("tts:opacity").value()); + } + if (node.attribute("arib-tt:border")) { + target.border = node.attribute("arib-tt:border").value(); + } + if (node.attribute("arib-tt:animation")) { + target.animation = TtmlStyleValueParser::parse(node.attribute("arib-tt:animation").value()); + } + if (node.attribute("arib-tt:ruby")) { + target.ruby = TtmlStyleValueParser::parse(node.attribute("arib-tt:ruby").value()); + } + if (node.attribute("arib-tt:text-shadow")) { + target.textShadow = node.attribute("arib-tt:text-shadow").value(); + } + if (node.attribute("tts:overflow")) { + target.overflow = TtmlStyleValueParser::parse(node.attribute("tts:overflow").value()); + } + applyTextOutline(node.attribute("tts:textOutline"), target); +} + +} TTML TTMLPaser::parse(const std::string& input) { TTML output; @@ -57,42 +225,53 @@ TTML TTMLPaser::parse(const std::string& input) { TTMLRegion region; region.id = p.attribute("xml:id").value(); if (p.attribute("tts:extent")) { - region.extent = TTMLCssValueParser::parsePair(p.attribute("tts:extent").value()); + const auto [width, height] = TtmlStyleValueParser::parseLengthPair(p.attribute("tts:extent").value()); + region.extent = TtmlStyleExtent{ width, height }; } if (p.attribute("tts:origin")) { - region.origin = TTMLCssValueParser::parsePair(p.attribute("tts:origin").value()); + const auto [x, y] = TtmlStyleValueParser::parseLengthPair(p.attribute("tts:origin").value()); + region.origin = TtmlStyleOrigin{ x, y }; } 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")) { + for (pugi::xml_node p : doc.child("tt").child("head").child("styling").children("style")) { + TTMLStyle style; + style.id = p.attribute("xml:id").value(); + applyInlineStyles(p, style); + + output.styles.push_back(style); + } + + // A style element may itself reference one or more styles. Resolve those + // references before content styles are computed; the element's own values + // have higher priority than its referenced styles. + for (pugi::xml_node node : doc.child("tt").child("head").child("styling").children("style")) { + const std::string id = node.attribute("xml:id").value(); + auto own = std::find_if(output.styles.begin(), output.styles.end(), [&id](const TTMLStyle& style) { + return style.id == id; + }); + if (own == output.styles.end() || !node.attribute("style")) { + continue; + } + TTMLStyle resolved; + applyReferencedStyles(node, output.styles, resolved); + mergeStyle(resolved, *own); + resolved.id = own->id; + *own = resolved; + } + + const pugi::xml_node body = doc.child("tt").child("body"); + TTMLStyle bodyStyle = initialStyle(); + applyReferencedStyles(body, output.styles, bodyStyle); + applyInlineStyles(body, bodyStyle); + + for (pugi::xml_node div : body.children("div")) { + TTMLDivTag divTag; + TTMLStyle divStyle = inheritedStyle(bodyStyle); + applyReferencedStyles(div, output.styles, divStyle); + applyInlineStyles(div, divStyle); + if (div.attribute("begin")) { divTag.begin = parseTimestamp(div.attribute("begin").value()); } if (div.attribute("end")) { @@ -105,48 +284,37 @@ TTML TTMLPaser::parse(const std::string& input) { return r.id == regionId; }); - TTMLPTag pTag; - pTag.id = p.attribute("xml:id").value(); + TTMLPTag pTag; + TTMLStyle pStyle = inheritedStyle(divStyle); + applyReferencedStyles(p, output.styles, pStyle); + applyInlineStyles(p, pStyle); + 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); - } + TTMLSpanTag spanTag; + spanTag.id = span.attribute("xml:id").value(); + spanTag.text = span.text().get(); + spanTag.style = inheritedStyle(pStyle); + applyReferencedStyles(span, output.styles, spanTag.style); + applyInlineStyles(span, spanTag.style); + completeComputedStyle(spanTag.style); + + pTag.spanTags.push_back(spanTag); + } + + if (pTag.spanTags.empty()) { + const std::string text = p.text().get(); + if (!text.empty()) { + TTMLSpanTag spanTag; + spanTag.text = text; + spanTag.style = inheritedStyle(pStyle); + completeComputedStyle(spanTag.style); + pTag.spanTags.push_back(std::move(spanTag)); + } + } divTag.pTags.push_back(pTag); } @@ -157,14 +325,28 @@ TTML TTMLPaser::parse(const std::string& input) { return output; } -TTMLCssValue TTMLCssValueParser::parse(const std::string& input) { +TtmlStyleValueColor TtmlStyleValueColor::parse(std::string_view text) { + // ARIB TR-B39 8.5.6.4 operates only #RRGGBB and #RRGGBBAA. + if ((text.size() != 7 && text.size() != 9) || text.front() != '#') { + throw std::invalid_argument("Invalid ARIB-TTML color. Expected #RRGGBB or #RRGGBBAA"); + } + + const unsigned long value = std::stoul(std::string(text.substr(1)), nullptr, 16); + const uint8_t alpha = text.size() == 7 ? 0xFF : static_cast(value & 0xFF); + const uint8_t blue = text.size() == 7 ? static_cast(value & 0xFF) : static_cast((value >> 8) & 0xFF); + const uint8_t green = text.size() == 7 ? static_cast((value >> 8) & 0xFF) : static_cast((value >> 16) & 0xFF); + const uint8_t red = text.size() == 7 ? static_cast((value >> 16) & 0xFF) : static_cast((value >> 24) & 0xFF); + return { red, green, blue, alpha }; +} + +TtmlStyleValue TtmlStyleValueParser::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])); + return TtmlStyleValue(TtmlStyleValueLength(std::stof(match[1]), match[2])); } else { throw std::invalid_argument("Invalid length value: " + input); @@ -172,15 +354,18 @@ TTMLCssValue TTMLCssValueParser::parse(const std::string& input) { } if (input.find("#") == 0) { - return TTMLCssValue(TTMLCssValueColor(input)); + return TtmlStyleValue(TtmlStyleValueColor::parse(input)); } - static const std::set validKeywords = { "bold", "italic", "normal", "none" }; + static const std::set validKeywords = { + "bold", "italic", "normal", "none", "underline", "lrtb", "tbrl", + "hidden", "visible", "ruby", "emphasis", "base", "text", "container" + }; if (validKeywords.find(input) != validKeywords.end()) { - return TTMLCssValue(TTMLCssValueKeyword(input)); + return TtmlStyleValue(TtmlStyleValueKeyword(input)); } - return TTMLCssValue(TTMLCssValueNumber(std::stof(input))); + return TtmlStyleValue(TtmlStyleValueNumber(std::stof(input))); } catch (const std::invalid_argument& e) { @@ -189,13 +374,24 @@ TTMLCssValue TTMLCssValueParser::parse(const std::string& input) { } } -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 +std::pair TtmlStyleValueParser::parseLengthPair(const std::string& input) { + std::istringstream iss(input); + std::string token1, token2; + if (!(iss >> token1)) { + throw std::invalid_argument("Failed to parse TTML value pair from: " + input); + } + if (!(iss >> token2)) { + token2 = token1; + } + const TtmlStyleValue value1 = parse(token1); + const TtmlStyleValue value2 = parse(token2); + if (!value1.isValue() || !value2.isValue()) { + throw std::invalid_argument("Expected TTML length pair: " + input); + } + return std::make_pair(value1.getValue(), value2.getValue()); +} + +TtmlStyleFontSize TtmlStyleValueParser::parseFontSize(const std::string& input) { + const auto [width, height] = parseLengthPair(input); + return { width, height }; +} diff --git a/src/ttml.h b/src/ttml.h index aabd147..6178b6f 100644 --- a/src/ttml.h +++ b/src/ttml.h @@ -7,33 +7,22 @@ #include #include #include -#include -#include +#include +#include +#include -class TTMLCssValueLength { -public: - TTMLCssValueLength(float v, const std::string& u) : value(v), unit(u) {} +struct TtmlStyleValueLength { + TtmlStyleValueLength(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); - } +struct TtmlStyleValueColor { + TtmlStyleValueColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : r(r), g(g), b(b), a(a) {} + + static TtmlStyleValueColor parse(std::string_view text); uint8_t r; uint8_t g; @@ -41,37 +30,40 @@ class TTMLCssValueColor { uint8_t a; }; -class TTMLCssValueKeyword { -public: - TTMLCssValueKeyword(const std::string& k) : keyword(k) {} +struct TtmlStyleValueKeyword { + TtmlStyleValueKeyword(const std::string& k) : keyword(k) {} std::string keyword; }; -class TTMLCssValueNumber { -public: - TTMLCssValueNumber(float n) : number(n) {} +struct TtmlStyleValueNumber { + TtmlStyleValueNumber(float n) : number(n) {} float number; }; -class TTMLCssValue { -public: - using ValueType = std::variant; +class TtmlStyleValue { +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); - } + TtmlStyleValue(TtmlStyleValueLength length) : value(length) {} + TtmlStyleValue(TtmlStyleValueColor color) : value(color) {} + TtmlStyleValue(TtmlStyleValueKeyword keyword) : value(keyword) {} + TtmlStyleValue(TtmlStyleValueNumber number) : value(number) {} + + template + T getValue() const { + return std::get(value); + } + + template + bool isValue() const { + return std::holds_alternative(value); + } template void setValue(const T& new_value) { @@ -79,30 +71,56 @@ class TTMLCssValue { } }; -using TTMLCssValuePair = std::pair; - -class TTMLCssValueParser { -public: - static TTMLCssValue parse(const std::string& input); - static TTMLCssValuePair parsePair(const std::string& input); +struct TtmlStyleFontSize { + TtmlStyleValueLength width; + TtmlStyleValueLength height; +}; + +struct TtmlStyleExtent { + TtmlStyleValueLength width; + TtmlStyleValueLength height; +}; + +struct TtmlStyleOrigin { + TtmlStyleValueLength x; + TtmlStyleValueLength y; +}; + +class TtmlStyleValueParser { +public: + static TtmlStyleValue parse(const std::string& input); + static TtmlStyleFontSize parseFontSize(const std::string& input); + static std::pair parseLengthPair(const std::string& input); }; struct TTMLRegion { std::string id; - std::optional> extent; - std::optional> origin; + 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 TTMLStyle { + std::string id; + std::optional fontSize; + std::optional lineHeight; + std::optional letterSpacing; + std::optional fontWeight; + std::optional fontStyle; + std::optional color; + std::optional backgroundColor; + bool outlineSpecified = false; + std::optional outlineColor; + std::optional fontFamily; + std::optional textDecoration; + std::optional writingMode; + std::optional opacity; + std::optional border; + std::optional animation; + std::optional ruby; + std::optional textShadow; + std::optional overflow; +}; struct TTMLSpanTag { std::string id; @@ -133,4 +151,4 @@ 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..d35ddbd --- /dev/null +++ b/src/ttml/ast.h @@ -0,0 +1,68 @@ +#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; + StyleProperties inline_style; + std::vector content; +}; + +struct Paragraph { + std::optional id; + Timing timing; + std::optional region; + std::vector style_refs; + StyleProperties inline_style; + std::vector spans; +}; + +struct Division { + Timing timing; + std::vector paragraphs; +}; + +struct Document { + std::vector styles; + std::vector regions; + std::vector divisions; +}; + +} // 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..b02b28e --- /dev/null +++ b/src/ttml/b24_converter.cpp @@ -0,0 +1,709 @@ +#include "ttml/b24_converter.h" +#include "aribTextEncoder.h" +#include "b24Color.h" +#include "b24ControlSet.h" + +#include +#include +#include +#include +#include + +namespace arib::ttml { + +namespace { + +struct StyledRun { + std::string text; + StyleProperties style; +}; + +struct Line { + std::vector runs; +}; + +struct RegionBlock { + resolved::Region region; + std::vector lines; +}; + +struct Page { + resolved::Timing timing; + std::vector blocks; +}; + +struct CollectResult { + std::vector pages; + std::optional error; +}; + +struct TimelineEvent { + std::chrono::milliseconds time; + std::vector starts; + std::vector ends; +}; + +struct RenderAction { + std::optional begin; + std::uint64_t delay; + bool clear; + std::vector pages; +}; + +bool same_timing(const resolved::Timing& lhs, const resolved::Timing& rhs) { + return lhs.begin == rhs.begin && lhs.end == rhs.end; +} + +std::uint64_t to_deciseconds(std::chrono::milliseconds time) { + return static_cast(time.count() / 100); +} + +std::size_t find_or_add_page(std::vector& pages, const resolved::Timing& timing) { + const auto found = std::find_if(pages.begin(), pages.end(), + [&](const Page& page) { return same_timing(page.timing, timing); }); + + if (found != pages.end()) { + return static_cast(std::distance(pages.begin(), found)); + } + + pages.push_back(Page{ .timing = timing }); + return pages.size() - 1; +} + +std::vector split_by_null(std::string_view data) { + std::vector output; + std::size_t begin = 0; + + for (std::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 PageBuilder { +public: + explicit PageBuilder(const resolved::Document& document) + : document_(document) { + } + + CollectResult run() const { + CollectResult output; + + for (const auto& division : document_.divisions) { + for (const auto& paragraph : division.paragraphs) { + std::vector> paragraph_block_indexes; + + for (const auto& span : paragraph.spans) { + const bool has_text = std::any_of(span.content.begin(), span.content.end(), + [](const auto& content) { + const auto* text = std::get_if(&content); + return text && !text->empty(); + }); + if (!has_text) { + continue; + } + + const resolved::Region* region = span.region + ? &*span.region + : paragraph.region ? &*paragraph.region : nullptr; + + if (!region) { + output.error = B24ConvertError{ "span has no effective region" }; + return output; + } + + const auto page_index = find_or_add_page(output.pages, span.timing); + Page& page = output.pages[page_index]; + if (paragraph_block_indexes.size() <= page_index) { + paragraph_block_indexes.resize(page_index + 1); + } + + RegionBlock* block; + if (span.region) { + page.blocks.push_back(RegionBlock{ + .region = *region, + .lines = { Line{} }, + }); + block = &page.blocks.back(); + } + else { + auto& block_index = paragraph_block_indexes[page_index]; + if (!block_index) { + page.blocks.push_back(RegionBlock{ + .region = *region, + .lines = { Line{} }, + }); + block_index = page.blocks.size() - 1; + } + block = &page.blocks[*block_index]; + } + + append_span(*block, span); + } + } + } + + std::stable_sort(output.pages.begin(), output.pages.end(), + [](const Page& lhs, const Page& rhs) { + if (!lhs.timing.begin) { + return rhs.timing.begin.has_value(); + } + if (!rhs.timing.begin) { + return false; + } + return *lhs.timing.begin < *rhs.timing.begin; + }); + + return output; + } + +private: + const resolved::Document& document_; +}; + +class TimelineBuilder { +public: + explicit TimelineBuilder(const std::vector& pages) + : pages_(pages) { + } + + std::vector run() const { + std::map events; + + for (const auto& page : pages_) { + const auto begin = page.timing.begin.value_or(std::chrono::milliseconds{}); + auto& start_event = events[begin]; + start_event.time = begin; + start_event.starts.push_back(&page); + + if (page.timing.end) { + const auto end = *page.timing.end; + auto& end_event = events[end]; + end_event.time = end; + end_event.ends.push_back(&page); + } + } + + std::vector output; + output.reserve(events.size()); + for (auto& entry : events) { + output.push_back(std::move(entry.second)); + } + return output; + } + +private: + const std::vector& pages_; +}; + +class Encoder { +public: + Encoder(const std::vector& pages, PESType type) + : pages_(pages), type_(type) { + } + + B24ConvertResult run() { + const auto actions = build_render_actions(); + if (type_ == PESType::Sync) { + if (actions.empty()) { + return { + .outputs = { B24ConvertOutput{ + .data = {}, + .begin = std::nullopt, + .clear = true, + } }, + .error = std::nullopt, + }; + } + return encode_sync(actions); + } + + append_initial_format(); + for (const auto& action : actions) { + auto encoded_runs = encode_text_action(action); + std::size_t encoded_run_index = 0; + append_time(action.delay); + if (action.clear) { + append_clear(); + append_initial_format(); + } + + for (const auto* page : action.pages) { + encode_page(*page, encoded_runs, encoded_run_index); + } + } + + return { + .outputs = { B24ConvertOutput{ + .data = std::move(output_), + .begin = pages_.empty() ? std::nullopt : pages_.front().timing.begin, + .clear = false, + } }, + .error = std::nullopt, + }; + } + +private: + B24ConvertResult encode_sync(const std::vector& actions) { + B24ConvertResult result; + for (std::size_t action_index = 0; action_index < actions.size(); ++action_index) { + const auto& action = actions[action_index]; + const bool is_last_clear = action_index + 1 == actions.size() + && action.clear + && action.pages.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(action); + std::size_t encoded_run_index = 0; + output_.clear(); + reset_state(); + append_initial_format(); + + for (const auto* page : action.pages) { + encode_page(*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_pages; + std::chrono::milliseconds current_time{}; + bool first_event = true; + + for (const auto& event : TimelineBuilder{pages_}.run()) { + const auto delay = first_event && type_ == PESType::Sync + ? 0 + : to_deciseconds(event.time) - to_deciseconds(current_time); + + for (const auto* page : event.ends) { + std::erase(active_pages, page); + } + for (const auto* page : event.starts) { + active_pages.push_back(page); + } + + const bool clear = !event.ends.empty(); + actions.push_back(RenderAction{ + .begin = first_event && !pages_.empty() && !pages_.front().timing.begin + ? std::nullopt + : std::optional{event.time}, + .delay = delay, + .clear = clear, + .pages = type_ == PESType::Sync + ? active_pages + : (clear ? active_pages : event.starts), + }); + + current_time = event.time; + first_event = false; + } + return actions; + } + + static void collect_text(const std::vector& pages, std::string& output) { + for (const auto* page : pages) { + 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_action(const RenderAction& action) { + std::string text; + collect_text(action.pages, text); + + return split_by_null(arib::text::encode(text, arib::charset::EncodeMode::Caption)); + } + + void encode_page(const Page& page, const std::vector& encoded_runs, std::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); + } + + append_aps(0, 0); + + for (std::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) { + if (run.style.letter_spacing) { + const auto spacing = static_cast(std::max(0.0, std::round(*run.style.letter_spacing * 960.0 / 3840.0))); + set_character_spacing(spacing); + } + + if (run.style.line_height && run.style.font_size) { + const double font_height = run.style.font_size->height; + const double line_height = std::holds_alternative(*run.style.line_height) + ? font_height * 1.2 + : std::get(*run.style.line_height); + const auto spacing = static_cast(std::max(0.0, std::round((line_height - font_height) * 540.0 / 2160.0))); + set_line_spacing(spacing); + } + + if (run.style.font_size) { + const double width = run.style.font_size->width; + const double height = run.style.font_size->height; + + uint32_t composition_width = static_cast(std::max(1.0, std::round(width * 960.0 / 3840.0))); + uint32_t composition_height = static_cast(std::max(1.0, std::round(height * 540.0 / 2160.0))); + uint8_t size = B24ControlSet::NSZ; + set_character_composition(composition_width, composition_height); + set_character_size(size); + } + + if (run.style.color) { + set_text_color(*run.style.color); + } + + if (run.style.background_color) { + set_background_color(*run.style.background_color); + } + + const bool underline = run.style.text_decoration == StyleTextDecorationValue::Underline; + set_underline(underline); + + uint8_t font = 0; + if (run.style.font_weight == StyleFontWeightValue::Bold) { + font |= 0x01; + } + if (run.style.font_style == StyleFontStyleValue::Italic) { + font |= 0x02; + } + set_font(font); + + if (!run.style.text_outline || + std::holds_alternative(*run.style.text_outline)) { + set_ornament_off(); + } + else { + const auto& text_outline = std::get(*run.style.text_outline); + const auto color = text_outline.color.value_or( + run.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); + } + 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) { + 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()); + } + + void append_extent(const StyleExtentValue& extent) { + output_.push_back(B24ControlSet::CSI); + append_number(static_cast(extent.width * 960 / 3840)); + output_.push_back(0x3B); + append_number(static_cast(extent.height * 540 / 2160)); + output_.push_back(B24ControlSet::SP); + output_.push_back(B24ControlSet::SDF); + } + + void append_origin(const StyleExtentValue& origin) { + output_.push_back(B24ControlSet::CSI); + append_number(static_cast(origin.width * 960 / 3840)); + output_.push_back(0x3B); + append_number(static_cast(origin.height * 540 / 2160)); + 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)); + 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& pages_; + PESType type_; + 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, PESType type) { + auto collected = PageBuilder{document}.run(); + if (collected.error) { + return { + .outputs = {}, + .error = std::move(collected.error), + }; + } + + return Encoder{collected.pages, type}.run(); +} + +} // namespace arib::ttml diff --git a/src/ttml/b24_converter.h b/src/ttml/b24_converter.h new file mode 100644 index 0000000..d362603 --- /dev/null +++ b/src/ttml/b24_converter.h @@ -0,0 +1,39 @@ +#pragma once + +#include "ttml/resolved.h" + +#include +#include +#include +#include +#include + +namespace arib::ttml { + +enum class PESType { + Sync, + Async, +}; + +struct B24ConvertError { + std::string message; +}; + +struct B24ConvertOutput { + std::vector data; + std::optional begin; + bool clear; +}; + +struct B24ConvertResult { + std::vector outputs; + std::optional error; + + bool has_error() const { + return error.has_value(); + } +}; + +B24ConvertResult convert_to_b24(const resolved::Document& document, PESType type); + +} // namespace arib::ttml diff --git a/src/ttml/parser.cpp b/src/ttml/parser.cpp new file mode 100644 index 0000000..6555d22 --- /dev/null +++ b/src/ttml/parser.cpp @@ -0,0 +1,663 @@ +#include "parser.h" +#include "ast.h" +#include "string_utils.h" +#include "pugixml.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +namespace { + +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 std::nullopt; + } + + auto parse_number = [](std::string_view s) -> std::optional { + int result = 0; + + for (const char c : s) { + if (c < '0' || c > '9') { + return std::nullopt; + } + + 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 std::nullopt; + } + + if (*m >= 60 || *s >= 60) { + return std::nullopt; + } + + using namespace std::chrono; + + return duration_cast( + hours{ *h } + + minutes{ *m } + + seconds{ *s } + ) + milliseconds{ *ms }; +} + +class Parser { +public: + explicit Parser(std::string_view xml) + : xml_(xml) { + } + + 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 = std::nullopt, + .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 = std::nullopt, + .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 = std::nullopt, + .error = make_error("Only one head element is allowed", child) + }; + } + } + else if (child_name == "body") { + if (body.empty()) { + body = child; + } + else { + return ParseResult{ + .document = std::nullopt, + .error = make_error("Only one body element is allowed", child) + }; + } + } + } + + if (!head.empty()) { + const auto error = parse_head(head); + if (error) { + return ParseResult{ + .document = std::nullopt, + .error = error + }; + } + } + if (!body.empty()) { + const auto error = parse_body(body); + if (error) { + return ParseResult{ + .document = std::nullopt, + .error = error + }; + } + } + + result.document = std::move(ast_doc_); + return result; + } + +private: + std::optional parse_body(pugi::xml_node body) { + int div_count = 0; + 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 (++div_count > 1) { + 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 error = parse_timing(body, division.timing); + if (error) { + return 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 error = parse_p(child, division); + if (error) { + return error; + } + } + } + + ast_doc_.divisions.push_back(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; + } + + const auto error = parse_timing(node, paragraph.timing); + if (error) { + return error; + } + + if (const auto error = parse_region_ref(node, paragraph.region)) { + return error; + } + + if (const auto error = parse_style_refs(node, paragraph.style_refs)) { + return error; + } + if (const auto error = parse_style_properties(node, paragraph.inline_style)) { + return error; + } + + for (auto child = node.first_child(); !child.empty(); child = child.next_sibling()) { + const 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; + } + if (const auto error = parse_style_properties(node, span.inline_style)) { + 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); + if (error) { + return error; + } + ast_doc_.styles.push_back(std::move(definition)); + } + + return {}; + } + + std::optional parse_style_properties(pugi::xml_node style, StyleProperties& output, bool is_region=false) { + 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 tts:backgroundColor value", style); + } + output.background_color = color; + } + else if (attr_name == "tts:color") { + const auto color = parse_color(attr_value); + if (!color) { + return make_error("invalid tts:color value", style); + } + output.color = color; + } + else if (attr_name == "tts:extent" && is_region) { + const auto extent = parse_length_pair(attr_value); + if (!extent) { + return make_error("invalid tts:extent value", style); + } + output.extent = extent; + } + else if (attr_name == "tts:fontFamily") { + if (attr_value.empty()) { + return make_error("invalid tts:fontFamily value", style); + } + output.font_family = attr_value; + } + else if (attr_name == "tts:fontSize") { + const auto font_size = parse_length_pair(attr_value); + if (!font_size) { + return make_error("invalid tts:fontSize value", 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 tts:fontStyle value", 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 tts:fontWeight value", 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) { + return make_error("invalid tts:lineHeight value", style); + } + output.line_height = *line_height; + } + } + else if (attr_name == "tts:origin" && is_region) { + const auto origin = parse_length_pair(attr_value); + if (!origin) { + return make_error("invalid tts:origin value", 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 tts:textDecoration value", style); + } + } + else if (attr_name == "tts:textOutline") { + const auto text_outline = parse_text_outline(attr_value); + if (!text_outline) { + return make_error("invalid tts:textOutline value", 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 tts:writingMode value", style); + } + } + else if (attr_name == "tts:opacity") { + const auto opacity = parse_length(attr_value); + if (!opacity) { + return make_error("invalid tts:opacity value", style); + } + if (*opacity < 0 || *opacity > 1) { + return make_error("invalid tts:opacity value", style); + } + + output.opacity = opacity; + } + else if (attr_name == "arib-tt:letter-spacing") { + const auto letter_spacing = parse_length(attr_value); + if (!letter_spacing) { + return make_error("invalid arib-tt:letter-spacing value", style); + } + if (*letter_spacing < 0) { + return make_error("invalid arib-tt:letter-spacing value", style); + } + + output.letter_spacing = letter_spacing; + } + else if (attr_name == "arib-tt:ruby") { + if (attr_value.empty()) { + return make_error("invalid arib-tt:ruby value", 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("", child); + } + + 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::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, true); + 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 (const auto begin_attr = node.attribute("begin")) { + auto begin = parse_time_expression(begin_attr.value()); + if (!begin) { + return make_error("invalid begin time", 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 end time", 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_; + ast::Document ast_doc_; + std::unordered_set ids_; +}; + +} + +ParseResult parse(std::string_view xml) { + return Parser{ xml }.run(); +} + +} + +} diff --git a/src/ttml/parser.h b/src/ttml/parser.h new file mode 100644 index 0000000..8492c9d --- /dev/null +++ b/src/ttml/parser.h @@ -0,0 +1,33 @@ +#pragma once +#include "ttml/style.h" +#include "ttml/ast.h" + +namespace arib { + +namespace ttml { + +struct SourceLocation { + std::size_t byte_offset{}; + std::uint32_t line{}; + std::uint32_t column{}; +}; + +struct ParseError { + SourceLocation location; + std::string message; +}; + +struct ParseResult { + std::optional document; + std::optional error; + + bool has_error() const { + return error.has_value(); + } +}; + +ParseResult parse(std::string_view xml); + +} // namespace ttml + +} // namespace arib \ No newline at end of file diff --git a/src/ttml/resolved.h b/src/ttml/resolved.h new file mode 100644 index 0000000..5818f0d --- /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::vector divisions; +}; + +} // namespace arib: + +} // namespace ttml + +} // namespace resolved diff --git a/src/ttml/resolver.cpp b/src/ttml/resolver.cpp new file mode 100644 index 0000000..123072a --- /dev/null +++ b/src/ttml/resolver.cpp @@ -0,0 +1,232 @@ +#include "ttml/resolver.h" + +#include +#include +#include + +namespace arib::ttml { + +namespace { + +void merge_style(StyleProperties& target, const StyleProperties& source) { +#define MERGE_PROPERTY(name) if (source.name) target.name = source.name + MERGE_PROPERTY(background_color); + MERGE_PROPERTY(color); + MERGE_PROPERTY(extent); + MERGE_PROPERTY(font_family); + MERGE_PROPERTY(font_size); + MERGE_PROPERTY(font_style); + MERGE_PROPERTY(font_weight); + MERGE_PROPERTY(line_height); + MERGE_PROPERTY(origin); + MERGE_PROPERTY(text_decoration); + MERGE_PROPERTY(text_outline); + MERGE_PROPERTY(writing_mode); + MERGE_PROPERTY(opacity); + MERGE_PROPERTY(letter_spacing); + MERGE_PROPERTY(ruby); +#undef MERGE_PROPERTY +} + +StyleProperties inherited_style(const StyleProperties& parent) { + StyleProperties result; + 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.opacity = parent.opacity; + 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, 255 } }, + .color = StyleColorValue{ { 255, 255, 255, 255 } }, + .font_family = "round gothic", + .font_size = StyleFontSizeValue{ 64.0, 64.0 }, + .font_style = StyleFontStyleValue::Normal, + .font_weight = StyleFontWeightValue::Normal, + .line_height = 64.0, + .text_decoration = StyleTextDecorationValue::None, + .text_outline = None{}, + .writing_mode = StyleWritingModeValue::Lrtb, + .opacity = 1.0, + .letter_spacing = 0.0, + }; + return style; +} + +void complete_style(StyleProperties& style) { + const auto& initial = initial_style(); +#define APPLY_INITIAL(name) if (!style.name) style.name = initial.name + APPLY_INITIAL(background_color); + APPLY_INITIAL(color); + APPLY_INITIAL(font_family); + APPLY_INITIAL(font_size); + APPLY_INITIAL(font_style); + APPLY_INITIAL(font_weight); + APPLY_INITIAL(line_height); + APPLY_INITIAL(text_decoration); + APPLY_INITIAL(text_outline); + APPLY_INITIAL(writing_mode); + APPLY_INITIAL(opacity); + APPLY_INITIAL(letter_spacing); +#undef APPLY_INITIAL +} + +resolved::Timing resolve_timing(const ast::Timing& timing, const resolved::Timing& parent) { + resolved::Timing result; + result.begin = timing.begin + ? std::optional{parent.begin.value_or(std::chrono::milliseconds{}) + *timing.begin} + : parent.begin; + result.end = timing.end + ? std::optional{parent.begin.value_or(std::chrono::milliseconds{}) + *timing.end} + : parent.end; + return result; +} + +class Resolver { +public: + explicit Resolver(const ast::Document& document) : document_(document) { + for (const auto& style : document.styles) styles_.emplace(style.id, &style); + for (const auto& region : document.regions) regions_.emplace(region.id, ®ion); + } + + ResolveResult run() { + resolved::Document output; + for (const auto& division : document_.divisions) { + resolved::Division resolved_division; + resolved_division.timing = resolve_timing(division.timing, {}); + + for (const auto& paragraph : division.paragraphs) { + resolved::Paragraph resolved_paragraph; + resolved_paragraph.id = paragraph.id; + resolved_paragraph.timing = resolve_timing(paragraph.timing, resolved_division.timing); + + if (!apply_refs(resolved_paragraph.style, paragraph.style_refs)) { + return failure(); + } + + merge_style(resolved_paragraph.style, paragraph.inline_style); + if (!resolve_region(paragraph.region, resolved_paragraph.region)) { + return failure(); + } + + if (resolved_paragraph.region) { + auto region_style = inherited_style(resolved_paragraph.region->style); + merge_style(region_style, resolved_paragraph.style); + resolved_paragraph.style = std::move(region_style); + } + + for (const auto& span : paragraph.spans) { + resolved::Span resolved_span; + resolved_span.id = span.id; + resolved_span.timing = resolve_timing(span.timing, resolved_paragraph.timing); + resolved_span.style = inherited_style(resolved_paragraph.style); + if (!apply_refs(resolved_span.style, span.style_refs)) { + return failure(); + } + + merge_style(resolved_span.style, span.inline_style); + if (!resolve_region(span.region, resolved_span.region)) { + return failure(); + } + + if (resolved_span.region) { + auto region_style = inherited_style(resolved_span.region->style); + merge_style(region_style, resolved_span.style); + resolved_span.style = std::move(region_style); + } + 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.divisions.push_back(std::move(resolved_division)); + } + return { + .document = std::move(output), + .error = std::nullopt + }; + } + +private: + bool apply_refs(StyleProperties& target, const std::vector& refs) { + for (const auto& ref : refs) { + StyleProperties resolved; + if (!resolve_style(ref, resolved)) return false; + merge_style(target, resolved); + } + return true; + } + + bool resolve_style(const std::string& id, StyleProperties& output) { + if (const auto cached = resolved_styles_.find(id); cached != resolved_styles_.end()) { + output = cached->second; + return true; + } + const auto found = styles_.find(id); + if (found == styles_.end()) { + error_ = ResolveError{ "unknown style reference: " + id }; + return false; + } + if (!resolving_styles_.emplace(id).second) { + error_ = ResolveError{ "cyclic style reference: " + id }; + return false; + } + + StyleProperties resolved; + if (!apply_refs(resolved, found->second->style_refs)) { + return false; + } + merge_style(resolved, found->second->style); + resolving_styles_.erase(id); + resolved_styles_.emplace(id, resolved); + output = std::move(resolved); + return true; + } + + bool resolve_region(const std::optional& id, std::optional& output) { + if (!id) return true; + const auto found = regions_.find(*id); + if (found == regions_.end()) { + error_ = ResolveError{ "unknown region reference: " + *id }; + return false; + } + StyleProperties style; + if (!apply_refs(style, found->second->style_refs)) { + return false; + } + merge_style(style, found->second->style); + output = resolved::Region{ found->second->id, std::move(style) }; + return true; + } + + ResolveResult failure() { + return { .document = std::nullopt, .error = std::move(error_) }; + } + + const ast::Document& document_; + std::unordered_map styles_; + std::unordered_map regions_; + std::unordered_map resolved_styles_; + std::unordered_set resolving_styles_; + std::optional error_; +}; + +} // namespace + +ResolveResult resolve(const ast::Document& document) { + return Resolver{document}.run(); +} + +} // namespace arib::ttml diff --git a/src/ttml/resolver.h b/src/ttml/resolver.h new file mode 100644 index 0000000..9ffd1a1 --- /dev/null +++ b/src/ttml/resolver.h @@ -0,0 +1,26 @@ +#pragma once + +#include "ttml/ast.h" +#include "ttml/resolved.h" + +#include +#include + +namespace arib::ttml { + +struct ResolveError { + std::string message; +}; + +struct ResolveResult { + std::optional document; + std::optional error; + + bool has_error() const { + return error.has_value(); + } +}; + +ResolveResult resolve(const ast::Document& document); + +} // namespace arib::ttml diff --git a/src/ttml/string_utils.h b/src/ttml/string_utils.h new file mode 100644 index 0000000..67f7ff4 --- /dev/null +++ b/src/ttml/string_utils.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +namespace arib::ttml { + +constexpr std::string_view kWhitespace = " \t\r\n"; + +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 arib::ttml diff --git a/src/ttml/style.cpp b/src/ttml/style.cpp new file mode 100644 index 0000000..d5c2d50 --- /dev/null +++ b/src/ttml/style.cpp @@ -0,0 +1,208 @@ +#include "ttml/style.h" +#include "ttml/string_utils.h" +#include +#include + +namespace arib { + +namespace ttml { + +namespace { + +bool parse_hex_bytes(std::string_view input, std::span output) { + if (input.size() > output.size() * 2) { + 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; + }; + + for (size_t i = 0; i < output.size(); ++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; + + // #RRGGBB + if (color_string.length() == 7) { + if (!parse_hex_bytes(color_string.substr(1), color.rgba)) { + return {}; + } + + color.rgba[3] = 0xFF; + } + + // #RRGGBBAA + if (color_string.length() == 9) { + if (!parse_hex_bytes(color_string.substr(1), color.rgba)) { + 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()); + + if (value.empty()) { + return {}; + } + + std::string temp{ value }; + + char* end = nullptr; + const double result = std::strtod(temp.c_str(), &end); + + if (end != temp.c_str() + temp.size()) { + return {}; + } + + return result; +} + +std::optional parse_length_pair(std::string_view length_string) { + 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 width = parse_length(split[0]); + if (!width) { + return {}; + } + + if (split.size() == 1) { + return LengthPair{ *width, *width }; + } + + const auto height = parse_length(split[1]); + if (!height) { + return {}; + } + + return LengthPair{ *width, *height }; +} + +std::optional parse_text_outline(std::string_view value_string) { + TextOutline text_outline; + + 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.size() == 1) { + const auto border_width = parse_length(split[0]); + if (!border_width) { + return {}; + } + + text_outline.border_width = *border_width; + return text_outline; + } + else if (split.size() == 2) { + const auto color = parse_color(split[0]); + if (color) { + text_outline.color = *color; + + const auto border_width = parse_length(split[1]); + if (!border_width) { + return {}; + } + + text_outline.border_width = *border_width; + } + else { + const auto border_width = parse_length(split[0]); + if (!border_width) { + return {}; + } + + text_outline.border_width = *border_width; + + const auto blur_width = parse_length(split[1]); + if (!blur_width) { + return {}; + } + + text_outline.blur_width = *blur_width; + } + return text_outline; + } + else if (split.size() == 3) { + const auto color = parse_color(split[0]); + if (!color) { + return {}; + } + text_outline.color = *color; + + const auto border_width = parse_length(split[1]); + if (!border_width) { + return {}; + } + + text_outline.border_width = *border_width; + + const auto blur_width = parse_length(split[2]); + if (!blur_width) { + return {}; + } + + text_outline.blur_width = *blur_width; + return text_outline; + } + + return {}; +} +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/style.h b/src/ttml/style.h new file mode 100644 index 0000000..83762a8 --- /dev/null +++ b/src/ttml/style.h @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include +#include + +namespace arib { + +namespace ttml { + +struct None {}; + +struct StyleColorValue { + std::array rgba; + + constexpr std::uint8_t r() const { return rgba[0]; } + constexpr std::uint8_t g() const { return rgba[1]; } + constexpr std::uint8_t b() const { return rgba[2]; } + constexpr std::uint8_t a() const { return rgba[3]; } + + friend constexpr bool operator==(const StyleColorValue&, const StyleColorValue&) = default; +}; + +struct LengthPair { + double width{}; + double height{}; +}; + +using StyleExtentValue = LengthPair; +using StyleFontSizeValue = LengthPair; + +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 opacity; + std::optional letter_spacing; + std::optional ruby; + +}; + +std::optional parse_color(std::string_view text); +std::optional parse_length(std::string_view value); +std::optional parse_length_pair(std::string_view length_string); +std::optional parse_text_outline(std::string_view value_string); + +} // namespace ttml + +} // namespace arib \ No newline at end of file From 851b5764ef7bb15e1b1d914b85a1f5144f12aef9 Mon Sep 17 00:00:00 2001 From: nekohkr Date: Wed, 19 Aug 2026 03:25:25 +0900 Subject: [PATCH 03/11] Improve ARIB-TTML compliance --- msvc/dantto4k.vcxproj | 3 +- msvc/dantto4k.vcxproj.filters | 5 +- src/b24SubtitleConverter.cpp | 174 +++++----- src/mhDataComponentDescriptor.cpp | 3 +- src/ttml/ast.h | 37 +-- src/ttml/b24_converter.cpp | 517 +++++++++++++++++------------- src/ttml/b24_converter.h | 20 +- src/ttml/parser.cpp | 457 ++++++++++++-------------- src/ttml/parser.h | 20 +- src/ttml/resolved.h | 2 +- src/ttml/resolver.cpp | 312 +++++++++++------- src/ttml/resolver.h | 13 +- src/ttml/string_utils.h | 30 +- src/ttml/style.cpp | 140 +++----- src/ttml/style.h | 46 +-- src/ttml/sync_mode.h | 14 + 16 files changed, 963 insertions(+), 830 deletions(-) create mode 100644 src/ttml/sync_mode.h diff --git a/msvc/dantto4k.vcxproj b/msvc/dantto4k.vcxproj index 7edefce..6f18160 100644 --- a/msvc/dantto4k.vcxproj +++ b/msvc/dantto4k.vcxproj @@ -385,8 +385,9 @@ + - \ No newline at end of file + diff --git a/msvc/dantto4k.vcxproj.filters b/msvc/dantto4k.vcxproj.filters index 9133df2..71d5949 100644 --- a/msvc/dantto4k.vcxproj.filters +++ b/msvc/dantto4k.vcxproj.filters @@ -705,6 +705,9 @@ dantto4k\ttml + + dantto4k\ttml + dantto4k\ttml @@ -771,4 +774,4 @@ {84aaa3a8-9e4a-4085-9dc5-db67d16195cc} - \ No newline at end of file + diff --git a/src/b24SubtitleConverter.cpp b/src/b24SubtitleConverter.cpp index 80574a6..fe8cfae 100644 --- a/src/b24SubtitleConverter.cpp +++ b/src/b24SubtitleConverter.cpp @@ -1,12 +1,12 @@ -#include "b24SubtitleConverter.h" -#include "aribTextEncoder.h" -#include "b24Color.h" -#include "b24ControlSet.h" +#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 +#include "ttml/b24_converter.h" +#include +#include namespace { @@ -28,7 +28,7 @@ std::vector splitByNull(const std::string& data) { return tokens; } -void appendNumber(std::vector& output, int n) { +void appendNumber(std::vector& output, int n) { if (n == 0) { output.push_back(0x30); return; @@ -39,93 +39,97 @@ void appendNumber(std::vector& output, int n) { n /= 10; } std::reverse(temp.begin(), temp.end()); - output.insert(output.end(), temp.begin(), temp.end()); -} - -void appendCharacterComposition(std::vector& output, uint32_t width, uint32_t height) { - output.push_back(B24ControlSet::CSI); - appendNumber(output, width); - output.push_back(0x3B); - appendNumber(output, height); - output.push_back(B24ControlSet::SP); - output.push_back(B24ControlSet::SSM); -} - -void appendSpacing(std::vector& output, uint32_t spacing, uint8_t control) { - output.push_back(B24ControlSet::CSI); - appendNumber(output, spacing); - output.push_back(B24ControlSet::SP); - output.push_back(control); -} - -void appendTwoDigits(std::vector& output, uint8_t value) { - output.push_back(static_cast(0x30 + value / 10)); - output.push_back(static_cast(0x30 + value % 10)); -} - -void appendOrnament(std::vector& output, uint8_t palette, uint8_t index) { - output.push_back(B24ControlSet::CSI); - output.push_back(0x31); - output.push_back(0x3B); - appendTwoDigits(output, palette); - appendTwoDigits(output, index); - output.push_back(B24ControlSet::SP); - output.push_back(B24ControlSet::ORN); -} - -void appendOrnamentOff(std::vector& output) { - output.push_back(B24ControlSet::CSI); - output.push_back(0x30); - output.push_back(B24ControlSet::SP); - output.push_back(B24ControlSet::ORN); -} - -} + output.insert(output.end(), temp.begin(), temp.end()); +} + +void appendCharacterComposition(std::vector& output, uint32_t width, uint32_t height) { + output.push_back(B24ControlSet::CSI); + appendNumber(output, width); + output.push_back(0x3B); + appendNumber(output, height); + output.push_back(B24ControlSet::SP); + output.push_back(B24ControlSet::SSM); +} + +void appendSpacing(std::vector& output, uint32_t spacing, uint8_t control) { + output.push_back(B24ControlSet::CSI); + appendNumber(output, spacing); + output.push_back(B24ControlSet::SP); + output.push_back(control); +} + +void appendTwoDigits(std::vector& output, uint8_t value) { + output.push_back(static_cast(0x30 + value / 10)); + output.push_back(static_cast(0x30 + value % 10)); +} + +void appendOrnament(std::vector& output, uint8_t palette, uint8_t index) { + output.push_back(B24ControlSet::CSI); + output.push_back(0x31); + output.push_back(0x3B); + appendTwoDigits(output, palette); + appendTwoDigits(output, index); + output.push_back(B24ControlSet::SP); + output.push_back(B24ControlSet::ORN); +} + +void appendOrnamentOff(std::vector& output) { + output.push_back(B24ControlSet::CSI); + output.push_back(0x30); + output.push_back(B24ControlSet::SP); + output.push_back(B24ControlSet::ORN); +} -bool B24SubtitleConverter::convert(const std::string& input, std::list& output, B24::PESData::PESType pesType) { - std::string_view ttml_input = input; - - auto parse_result = arib::ttml::parse(ttml_input); +} + +bool B24SubtitleConverter::convert(const std::string& input, std::list& output, B24::PESData::PESType pesType) { + 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); + 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, - pesType == B24::PESData::PESType::Synchronized - ? arib::ttml::PESType::Sync - : arib::ttml::PESType::Async); + auto convert_result = arib::ttml::convert_to_b24(*resolve_result.document, sync_mode); 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; -} + + 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/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/ttml/ast.h b/src/ttml/ast.h index d35ddbd..4729390 100644 --- a/src/ttml/ast.h +++ b/src/ttml/ast.h @@ -17,36 +17,33 @@ struct Timing { 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 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; +struct Span { + std::optional id; Timing timing; std::optional region; std::vector style_refs; - StyleProperties inline_style; std::vector content; }; -struct Paragraph { - std::optional id; +struct Paragraph { + std::optional id; Timing timing; std::optional region; - std::vector style_refs; - StyleProperties inline_style; std::vector spans; }; @@ -58,11 +55,11 @@ struct Division { struct Document { std::vector styles; std::vector regions; - std::vector divisions; + std::optional division; }; } // namespace ast } // namespace ttml -} // namespace arib +} // namespace arib diff --git a/src/ttml/b24_converter.cpp b/src/ttml/b24_converter.cpp index b02b28e..e52d10d 100644 --- a/src/ttml/b24_converter.cpp +++ b/src/ttml/b24_converter.cpp @@ -2,20 +2,27 @@ #include "aribTextEncoder.h" #include "b24Color.h" #include "b24ControlSet.h" - #include -#include +#include +#include +#include +#include #include +#include #include #include -namespace arib::ttml { +namespace arib { + +namespace ttml { namespace { +constexpr double kLayoutScale = 0.25; + struct StyledRun { - std::string text; - StyleProperties style; + std::string_view text; + const StyleProperties* style; }; struct Line { @@ -23,58 +30,55 @@ struct Line { }; struct RegionBlock { - resolved::Region region; + const resolved::Region* region; std::vector lines; }; struct Page { - resolved::Timing timing; 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 pages; + std::vector cues; std::optional error; }; struct TimelineEvent { std::chrono::milliseconds time; - std::vector starts; - std::vector ends; + std::vector starts; + std::vector ends; }; struct RenderAction { std::optional begin; - std::uint64_t delay; + uint64_t delay; bool clear; - std::vector pages; + Page page; }; -bool same_timing(const resolved::Timing& lhs, const resolved::Timing& rhs) { - return lhs.begin == rhs.begin && lhs.end == rhs.end; -} - -std::uint64_t to_deciseconds(std::chrono::milliseconds time) { - return static_cast(time.count() / 100); -} - -std::size_t find_or_add_page(std::vector& pages, const resolved::Timing& timing) { - const auto found = std::find_if(pages.begin(), pages.end(), - [&](const Page& page) { return same_timing(page.timing, timing); }); - - if (found != pages.end()) { - return static_cast(std::distance(pages.begin(), found)); - } - - pages.push_back(Page{ .timing = timing }); - return pages.size() - 1; +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; - std::size_t begin = 0; + output.reserve(static_cast(std::ranges::count(data, '\0'))); + size_t begin = 0; - for (std::size_t i = 0; i < data.size(); ++i) { + for (size_t i = 0; i < data.size(); ++i) { if (data[i] == '\0') { output.emplace_back(data.substr(begin, i - begin)); begin = i + 1; @@ -97,7 +101,7 @@ void append_span(RegionBlock& block, const resolved::Span& span) { if (!text->empty()) { block.lines.back().runs.push_back(StyledRun{ .text = *text, - .style = span.style, + .style = &span.style, }); } } @@ -107,107 +111,150 @@ void append_span(RegionBlock& block, const resolved::Span& span) { } } -class PageBuilder { +class CueCollector { public: - explicit PageBuilder(const resolved::Document& document) - : document_(document) { + CueCollector(const resolved::Document& document, SyncMode mode) + : document_(document), mode_(mode) { } CollectResult run() const { CollectResult output; + size_t order = 0; - for (const auto& division : document_.divisions) { - for (const auto& paragraph : division.paragraphs) { - std::vector> paragraph_block_indexes; + if (!document_.division) { + return output; + } - for (const auto& span : paragraph.spans) { - const bool has_text = std::any_of(span.content.begin(), span.content.end(), - [](const auto& content) { - const auto* text = std::get_if(&content); - return text && !text->empty(); - }); - if (!has_text) { - continue; - } + 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 resolved::Region* region = span.region - ? &*span.region - : paragraph.region ? &*paragraph.region : nullptr; + 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; + } - if (!region) { - output.error = B24ConvertError{ "span has no effective region" }; - return output; - } + for (const auto& span : paragraph.spans) { + if (mode_ == SyncMode::Sync && span.timing.begin && span.timing.end && + *span.timing.end <= *span.timing.begin) { + continue; + } - const auto page_index = find_or_add_page(output.pages, span.timing); - Page& page = output.pages[page_index]; - if (paragraph_block_indexes.size() <= page_index) { - paragraph_block_indexes.resize(page_index + 1); + 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; + } - RegionBlock* block; - if (span.region) { - page.blocks.push_back(RegionBlock{ - .region = *region, - .lines = { Line{} }, - }); - block = &page.blocks.back(); + 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; } - else { - auto& block_index = paragraph_block_indexes[page_index]; - if (!block_index) { - page.blocks.push_back(RegionBlock{ - .region = *region, - .lines = { Line{} }, - }); - block_index = page.blocks.size() - 1; - } - block = &page.blocks[*block_index]; + if (has_origin) { + paragraph_region = &*paragraph.region; } + } - append_span(*block, span); + 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; + } } - } - } - std::stable_sort(output.pages.begin(), output.pages.end(), - [](const Page& lhs, const Page& rhs) { - if (!lhs.timing.begin) { - return rhs.timing.begin.has_value(); + if (!region) { + // A span region without geometry is style-only and continues in the paragraph region. + region = paragraph_region; } - if (!rhs.timing.begin) { - return false; + + if (!region) { + output.error = B24ConvertError{ "span has no effective region" }; + return output; } - return *lhs.timing.begin < *rhs.timing.begin; - }); + + 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& pages) - : pages_(pages) { + explicit TimelineBuilder(const std::vector& cues) + : cues_(cues) { } std::vector run() const { std::map events; - for (const auto& page : pages_) { - const auto begin = page.timing.begin.value_or(std::chrono::milliseconds{}); + 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(&page); + start_event.starts.push_back(&cue); - if (page.timing.end) { - const auto end = *page.timing.end; + 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(&page); + end_event.ends.push_back(&cue); } } @@ -220,64 +267,91 @@ class TimelineBuilder { } private: - const std::vector& pages_; + const std::vector& cues_; }; class Encoder { public: - Encoder(const std::vector& pages, PESType type) - : pages_(pages), type_(type) { + Encoder(const std::vector& cues, SyncMode mode) + : cues_(cues), mode_(mode) { } B24ConvertResult run() { - const auto actions = build_render_actions(); - if (type_ == PESType::Sync) { - if (actions.empty()) { - return { - .outputs = { B24ConvertOutput{ - .data = {}, - .begin = std::nullopt, - .clear = true, - } }, - .error = std::nullopt, - }; - } - return encode_sync(actions); + if (mode_ == SyncMode::Async) { + return encode_async(); + } + else { + return encode_sync(); } + } - append_initial_format(); - for (const auto& action : actions) { - auto encoded_runs = encode_text_action(action); - std::size_t encoded_run_index = 0; - append_time(action.delay); - if (action.clear) { - append_clear(); - append_initial_format(); +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; } - for (const auto* page : action.pages) { - encode_page(*page, encoded_runs, encoded_run_index); - } + 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 = pages_.empty() ? std::nullopt : pages_.front().timing.begin, + .begin = {}, .clear = false, } }, - .error = std::nullopt, + .error = {}, }; } -private: - B24ConvertResult encode_sync(const std::vector& actions) { + B24ConvertResult encode_sync() { + const auto actions = build_render_actions(); + if (actions.empty()) { + return { + .outputs = { B24ConvertOutput{ + .data = {}, + .begin = {}, + .clear = true, + } }, + .error = {}, + }; + } + B24ConvertResult result; - for (std::size_t action_index = 0; action_index < actions.size(); ++action_index) { - const auto& action = actions[action_index]; - const bool is_last_clear = action_index + 1 == actions.size() + 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.pages.empty() + && action.page.blocks.empty() && !result.outputs.empty(); if (is_last_clear) { output_ = std::move(result.outputs.back().data); @@ -287,15 +361,13 @@ class Encoder { continue; } - auto encoded_runs = encode_text_action(action); - std::size_t encoded_run_index = 0; + auto encoded_runs = encode_text(action.page); + size_t encoded_run_index = 0; output_.clear(); reset_state(); append_initial_format(); - for (const auto* page : action.pages) { - encode_page(*page, encoded_runs, encoded_run_index); - } + encode_page(action.page, encoded_runs, encoded_run_index); result.outputs.push_back(B24ConvertOutput{ .data = std::move(output_), @@ -309,32 +381,39 @@ class Encoder { std::vector build_render_actions() const { std::vector actions; - std::vector active_pages; + std::vector active_cues; std::chrono::milliseconds current_time{}; bool first_event = true; - for (const auto& event : TimelineBuilder{pages_}.run()) { - const auto delay = first_event && type_ == PESType::Sync - ? 0 - : to_deciseconds(event.time) - to_deciseconds(current_time); + active_cues.reserve(cues_.size()); - for (const auto* page : event.ends) { - std::erase(active_pages, page); + 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* page : event.starts) { - active_pages.push_back(page); + 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 clear = !event.ends.empty(); + 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 && !pages_.empty() && !pages_.front().timing.begin - ? std::nullopt - : std::optional{event.time}, + .begin = first_event && starts_without_time ? std::nullopt : std::optional{event.time}, .delay = delay, - .clear = clear, - .pages = type_ == PESType::Sync - ? active_pages - : (clear ? active_pages : event.starts), + .clear = !first_event, + .page = build_page(active_cues), }); current_time = event.time; @@ -343,99 +422,99 @@ class Encoder { return actions; } - static void collect_text(const std::vector& pages, std::string& output) { - for (const auto* page : pages) { - 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 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_action(const RenderAction& action) { + static std::vector encode_text(const Page& page) { std::string text; - collect_text(action.pages, 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, std::size_t& encoded_run_index) { + 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.extent) { + append_extent(*block.region->style.extent); } - if (block.region.style.origin) { - append_origin(*block.region.style.origin); + if (block.region->style.origin) { + append_origin(*block.region->style.origin); } - append_aps(0, 0); - - for (std::size_t line_index = 0; line_index < block.lines.size(); ++line_index) { + 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) { - if (run.style.letter_spacing) { - const auto spacing = static_cast(std::max(0.0, std::round(*run.style.letter_spacing * 960.0 / 3840.0))); + const auto& style = *run.style; + if (style.letter_spacing) { + const auto spacing = static_cast(std::max(0.0, std::round(*style.letter_spacing * kLayoutScale))); set_character_spacing(spacing); } - if (run.style.line_height && run.style.font_size) { - const double font_height = run.style.font_size->height; - const double line_height = std::holds_alternative(*run.style.line_height) + if (style.line_height && style.font_size) { + const double font_height = style.font_size->y; + const double line_height = std::holds_alternative(*style.line_height) ? font_height * 1.2 - : std::get(*run.style.line_height); - const auto spacing = static_cast(std::max(0.0, std::round((line_height - font_height) * 540.0 / 2160.0))); + : std::get(*style.line_height); + const auto spacing = static_cast(std::max(0.0, std::round((line_height - font_height) * kLayoutScale))); set_line_spacing(spacing); } - if (run.style.font_size) { - const double width = run.style.font_size->width; - const double height = run.style.font_size->height; - - uint32_t composition_width = static_cast(std::max(1.0, std::round(width * 960.0 / 3840.0))); - uint32_t composition_height = static_cast(std::max(1.0, std::round(height * 540.0 / 2160.0))); - uint8_t size = B24ControlSet::NSZ; + 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::max(1.0, std::round(font_width * kLayoutScale))); + uint32_t composition_height = static_cast(std::max(1.0, std::round(font_height * kLayoutScale))); set_character_composition(composition_width, composition_height); - set_character_size(size); + set_character_size(B24ControlSet::NSZ); } - if (run.style.color) { - set_text_color(*run.style.color); + if (style.color) { + set_text_color(*style.color); } - if (run.style.background_color) { - set_background_color(*run.style.background_color); + if (style.background_color) { + set_background_color(*style.background_color); } - const bool underline = run.style.text_decoration == StyleTextDecorationValue::Underline; + const bool underline = style.text_decoration == StyleTextDecorationValue::Underline; set_underline(underline); uint8_t font = 0; - if (run.style.font_weight == StyleFontWeightValue::Bold) { + if (style.font_weight == StyleFontWeightValue::Bold) { font |= 0x01; } - if (run.style.font_style == StyleFontStyleValue::Italic) { + if (style.font_style == StyleFontStyleValue::Italic) { font |= 0x02; } set_font(font); - if (!run.style.text_outline || - std::holds_alternative(*run.style.text_outline)) { + if (!style.text_outline || std::holds_alternative(*style.text_outline)) { set_ornament_off(); } else { - const auto& text_outline = std::get(*run.style.text_outline); + const auto& text_outline = std::get(*style.text_outline); const auto color = text_outline.color.value_or( - run.style.color.value_or(StyleColorValue{ { 255, 255, 255, 255 } })); + 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 (!aps) { + append_aps(0, 0); + aps = true; + } const auto& encoded = encoded_runs.at(encoded_run_index++); output_.insert(output_.end(), encoded.begin(), encoded.end()); } @@ -446,7 +525,6 @@ class Encoder { } } - void reset_state() { last_text_color_palette_ = 0; last_text_color_index_ = 7; @@ -477,33 +555,29 @@ class Encoder { } void append_number(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; + 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)); } - std::reverse(temp.begin(), temp.end()); - output_.insert(output_.end(), temp.begin(), temp.end()); } - void append_extent(const StyleExtentValue& extent) { + void append_extent(const LengthPair& extent) { output_.push_back(B24ControlSet::CSI); - append_number(static_cast(extent.width * 960 / 3840)); + append_number(static_cast(extent.x * kLayoutScale)); output_.push_back(0x3B); - append_number(static_cast(extent.height * 540 / 2160)); + append_number(static_cast(extent.y * kLayoutScale)); output_.push_back(B24ControlSet::SP); output_.push_back(B24ControlSet::SDF); } - void append_origin(const StyleExtentValue& origin) { + void append_origin(const LengthPair& origin) { output_.push_back(B24ControlSet::CSI); - append_number(static_cast(origin.width * 960 / 3840)); + append_number(static_cast(std::lround(origin.x * kLayoutScale))); output_.push_back(0x3B); - append_number(static_cast(origin.height * 540 / 2160)); + append_number(static_cast(std::lround(origin.y * kLayoutScale))); output_.push_back(B24ControlSet::SP); output_.push_back(B24ControlSet::SDP); } @@ -662,8 +736,7 @@ class Encoder { void append_time(std::uint64_t duration) { while (duration > 0) { - const auto value = static_cast( - std::min(duration, 0x3F)); + const auto value = static_cast(std::min(duration, 0x3F)); output_.push_back(B24ControlSet::TIME); output_.push_back(0x20); output_.push_back(0x40 | value); @@ -672,8 +745,8 @@ class Encoder { } private: - const std::vector& pages_; - PESType type_; + const std::vector& cues_; + SyncMode mode_; std::vector output_; uint8_t last_text_color_palette_{0}; uint8_t last_text_color_index_{7}; @@ -694,8 +767,8 @@ class Encoder { } // namespace -B24ConvertResult convert_to_b24(const resolved::Document& document, PESType type) { - auto collected = PageBuilder{document}.run(); +B24ConvertResult convert_to_b24(const resolved::Document& document, SyncMode mode) { + auto collected = CueCollector{document, mode}.run(); if (collected.error) { return { .outputs = {}, @@ -703,7 +776,9 @@ B24ConvertResult convert_to_b24(const resolved::Document& document, PESType type }; } - return Encoder{collected.pages, type}.run(); + return Encoder{collected.cues, mode}.run(); } -} // namespace arib::ttml +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/b24_converter.h b/src/ttml/b24_converter.h index d362603..0ac15a8 100644 --- a/src/ttml/b24_converter.h +++ b/src/ttml/b24_converter.h @@ -1,6 +1,7 @@ #pragma once #include "ttml/resolved.h" +#include "ttml/sync_mode.h" #include #include @@ -8,19 +9,16 @@ #include #include -namespace arib::ttml { +namespace arib { -enum class PESType { - Sync, - Async, -}; +namespace ttml { struct B24ConvertError { std::string message; }; struct B24ConvertOutput { - std::vector data; + std::vector data; std::optional begin; bool clear; }; @@ -29,11 +27,15 @@ struct B24ConvertResult { std::vector outputs; std::optional error; - bool has_error() const { + [[nodiscard]] bool has_error() const noexcept { return error.has_value(); } }; -B24ConvertResult convert_to_b24(const resolved::Document& document, PESType type); +[[nodiscard]] B24ConvertResult convert_to_b24( + const resolved::Document& document, + SyncMode mode); + +} // namespace ttml -} // namespace arib::ttml +} // namespace arib diff --git a/src/ttml/parser.cpp b/src/ttml/parser.cpp index 6555d22..da0b94d 100644 --- a/src/ttml/parser.cpp +++ b/src/ttml/parser.cpp @@ -1,20 +1,14 @@ -#include "parser.h" -#include "ast.h" -#include "string_utils.h" +#include "parser.h" +#include "ast.h" +#include "string_utils.h" #include "pugixml.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include -#include #include namespace arib { @@ -23,6 +17,11 @@ 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(); @@ -33,12 +32,7 @@ SourceLocation get_source_location(const pugi::xml_node& node, std::string_view uint32_t line = 1; uint32_t column = 1; - const auto end = - std::min( - static_cast(offset), - source.size() - ); - + const auto end = std::min(static_cast(offset), source.size()); for (size_t i = 0; i < end; ++i) { if (source[i] == '\n') { ++line; @@ -57,11 +51,8 @@ SourceLocation get_source_location(const pugi::xml_node& node, std::string_view } std::optional parse_time_expression(std::string_view value) { - if (value.size() != 12 || - value[2] != ':' || - value[5] != ':' || - value[8] != '.') { - return std::nullopt; + if (value.size() != 12 || value[2] != ':' || value[5] != ':' || value[8] != '.') { + return {}; } auto parse_number = [](std::string_view s) -> std::optional { @@ -69,7 +60,7 @@ std::optional parse_time_expression(std::string_view for (const char c : s) { if (c < '0' || c > '9') { - return std::nullopt; + return {}; } result = result * 10 + (c - '0'); @@ -82,28 +73,24 @@ std::optional parse_time_expression(std::string_view 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 std::nullopt; - } - - if (*m >= 60 || *s >= 60) { - return std::nullopt; + return {}; } - using namespace std::chrono; + if (*h >= 24 || *m >= 60 || *s >= 60) { + return {}; + } - return duration_cast( - hours{ *h } + - minutes{ *m } + - seconds{ *s } - ) + milliseconds{ *ms }; + return std::chrono::hours{*h} + + std::chrono::minutes{*m} + + std::chrono::seconds{*s} + + std::chrono::milliseconds{*ms}; } class Parser { public: - explicit Parser(std::string_view xml) - : xml_(xml) { + Parser(std::string_view xml, SyncMode mode) + : xml_(xml), mode_(mode) { } ParseResult run() { @@ -112,8 +99,8 @@ class Parser { pugi::xml_parse_result xml = doc.load_buffer(xml_.data(), xml_.size()); if (xml.status != pugi::status_ok) { return ParseResult{ - .document = std::nullopt, - .error = make_error(std::string("Malformed XML: ") + xml.description()), + .document = {}, + .error = make_error(std::string("malformed xml: ") + xml.description()), }; } @@ -128,8 +115,8 @@ class Parser { const std::string_view root_name = root.name(); if (root_name != "tt") { return ParseResult{ - .document = std::nullopt, - .error = make_error("Root element must be tt", root) + .document = {}, + .error = make_error("root element must be tt", root) }; } @@ -143,8 +130,8 @@ class Parser { } else { return ParseResult{ - .document = std::nullopt, - .error = make_error("Only one head element is allowed", child) + .document = {}, + .error = make_error("only one head element is allowed", child) }; } } @@ -154,8 +141,8 @@ class Parser { } else { return ParseResult{ - .document = std::nullopt, - .error = make_error("Only one body element is allowed", child) + .document = {}, + .error = make_error("only one body element is allowed", child) }; } } @@ -165,19 +152,19 @@ class Parser { const auto error = parse_head(head); if (error) { return ParseResult{ - .document = std::nullopt, + .document = {}, .error = error }; } } - if (!body.empty()) { - const auto error = parse_body(body); - if (error) { - return ParseResult{ - .document = std::nullopt, - .error = error - }; - } + if (!body.empty()) { + const auto error = parse_body(body); + if (error) { + return ParseResult{ + .document = {}, + .error = error + }; + } } result.document = std::move(ast_doc_); @@ -186,12 +173,11 @@ class Parser { private: std::optional parse_body(pugi::xml_node body) { - int div_count = 0; 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 (++div_count > 1) { - return make_error("Only one div element is allowed in body element", child); + if (ast_doc_.division) { + return make_error("only one div element is allowed in body element", child); } const auto error = parse_div(child); @@ -207,22 +193,22 @@ class Parser { std::optional parse_div(pugi::xml_node body) { ast::Division division; - const auto error = parse_timing(body, division.timing); - if (error) { - return error; + 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 error = parse_p(child, division); - if (error) { - return error; + const auto paragraph_error = parse_p(child, division); + if (paragraph_error) { + return paragraph_error; } } } - ast_doc_.divisions.push_back(division); + ast_doc_.division = std::move(division); return {}; } @@ -230,37 +216,28 @@ class Parser { 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 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); + } - const auto error = parse_timing(node, paragraph.timing); - if (error) { - return error; + 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; } - if (const auto error = parse_style_refs(node, paragraph.style_refs)) { - return error; - } - if (const auto error = parse_style_properties(node, paragraph.inline_style)) { - return error; - } - - for (auto child = node.first_child(); !child.empty(); child = child.next_sibling()) { - const std::string_view child_name = child.name(); + 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) { @@ -277,55 +254,50 @@ class Parser { 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 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_region_ref(node, span.region)) { + return error; } if (const auto error = parse_style_refs(node, span.style_refs)) { return error; } - if (const auto error = parse_style_properties(node, span.inline_style)) { - 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 {}; + 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) { @@ -369,13 +341,14 @@ class Parser { 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); + 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; } @@ -385,44 +358,46 @@ class Parser { return {}; } - std::optional parse_style_properties(pugi::xml_node style, StyleProperties& output, bool is_region=false) { + 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 tts:backgroundColor value", style); + 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 tts:color value", style); + return make_error("invalid value for tts:color", style); } output.color = color; - } - else if (attr_name == "tts:extent" && is_region) { - const auto extent = parse_length_pair(attr_value); - if (!extent) { - return make_error("invalid tts:extent value", style); - } - output.extent = extent; + } + 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 tts:fontFamily value", style); + 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); - if (!font_size) { - return make_error("invalid tts:fontSize value", style); - } - output.font_size = font_size; + 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") { @@ -432,7 +407,7 @@ class Parser { output.font_style = StyleFontStyleValue::Italic; } else { - return make_error("invalid tts:fontStyle value", style); + return make_error("invalid value for tts:fontstyle", style); } } else if (attr_name == "tts:fontWeight") { @@ -443,27 +418,31 @@ class Parser { output.font_weight = StyleFontWeightValue::Bold; } else { - return make_error("invalid tts:fontWeight value", style); + 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) { - return make_error("invalid tts:lineHeight value", style); - } - output.line_height = *line_height; - } - } - else if (attr_name == "tts:origin" && is_region) { - const auto origin = parse_length_pair(attr_value); - if (!origin) { - return make_error("invalid tts:origin value", style); + 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; } - output.origin = origin; + } + 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") { @@ -473,14 +452,17 @@ class Parser { output.text_decoration = StyleTextDecorationValue::Underline; } else { - return make_error("invalid tts:textDecoration value", style); + 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) { - return make_error("invalid tts:textOutline value", 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; } @@ -492,87 +474,71 @@ class Parser { output.writing_mode = StyleWritingModeValue::Tbrl; } else { - return make_error("invalid tts:writingMode value", style); + return make_error("invalid value for tts:writingmode", style); } } - else if (attr_name == "tts:opacity") { - const auto opacity = parse_length(attr_value); - if (!opacity) { - return make_error("invalid tts:opacity value", style); - } - if (*opacity < 0 || *opacity > 1) { - return make_error("invalid tts:opacity value", style); - } - - output.opacity = opacity; - } else if (attr_name == "arib-tt:letter-spacing") { const auto letter_spacing = parse_length(attr_value); if (!letter_spacing) { - return make_error("invalid arib-tt:letter-spacing value", style); + return make_error("invalid value for arib-tt:letter-spacing", style); } if (*letter_spacing < 0) { - return make_error("invalid arib-tt:letter-spacing value", style); + 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 arib-tt:ruby value", style); + 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 {}; } - 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) { + 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("", child); + 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("style element requires xml:id", child); + return make_error("region element requires xml:id", child); } const std::string_view id = id_attr.value(); @@ -584,13 +550,13 @@ class Parser { 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, true); + 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; } @@ -601,10 +567,14 @@ class Parser { } 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 begin time", node); + return make_error("invalid value for begin", node); } timing.begin = *begin; @@ -613,7 +583,7 @@ class Parser { if (const auto end_attr = node.attribute("end")) { auto end = parse_time_expression(end_attr.value()); if (!end) { - return make_error("invalid end time", node); + return make_error("invalid value for end", node); } timing.end = *end; @@ -637,9 +607,7 @@ class Parser { }; } - ParseError make_error( - std::string message, - const pugi::xml_node& node) const { + ParseError make_error(std::string message, const pugi::xml_node& node) const { return { .location = get_source_location(node, xml_), .message = std::move(message), @@ -648,16 +616,17 @@ class Parser { private: std::string xml_; + SyncMode mode_; ast::Document ast_doc_; std::unordered_set ids_; }; } -ParseResult parse(std::string_view xml) { - return Parser{ xml }.run(); +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 index 8492c9d..a5b5680 100644 --- a/src/ttml/parser.h +++ b/src/ttml/parser.h @@ -1,15 +1,21 @@ #pragma once -#include "ttml/style.h" #include "ttml/ast.h" +#include "ttml/sync_mode.h" + +#include +#include +#include +#include +#include namespace arib { namespace ttml { struct SourceLocation { - std::size_t byte_offset{}; - std::uint32_t line{}; - std::uint32_t column{}; + size_t byte_offset{}; + uint32_t line{}; + uint32_t column{}; }; struct ParseError { @@ -21,13 +27,13 @@ struct ParseResult { std::optional document; std::optional error; - bool has_error() const { + [[nodiscard]] bool has_error() const noexcept { return error.has_value(); } }; -ParseResult parse(std::string_view xml); +[[nodiscard]] ParseResult parse(std::string_view xml, SyncMode mode); } // namespace ttml -} // namespace arib \ No newline at end of file +} // namespace arib diff --git a/src/ttml/resolved.h b/src/ttml/resolved.h index 5818f0d..e9a5ea6 100644 --- a/src/ttml/resolved.h +++ b/src/ttml/resolved.h @@ -45,7 +45,7 @@ struct Division { }; struct Document { - std::vector divisions; + std::optional division; }; } // namespace arib: diff --git a/src/ttml/resolver.cpp b/src/ttml/resolver.cpp index 123072a..096dd6e 100644 --- a/src/ttml/resolver.cpp +++ b/src/ttml/resolver.cpp @@ -2,34 +2,49 @@ #include #include +#include #include -namespace arib::ttml { +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) { -#define MERGE_PROPERTY(name) if (source.name) target.name = source.name - MERGE_PROPERTY(background_color); - MERGE_PROPERTY(color); - MERGE_PROPERTY(extent); - MERGE_PROPERTY(font_family); - MERGE_PROPERTY(font_size); - MERGE_PROPERTY(font_style); - MERGE_PROPERTY(font_weight); - MERGE_PROPERTY(line_height); - MERGE_PROPERTY(origin); - MERGE_PROPERTY(text_decoration); - MERGE_PROPERTY(text_outline); - MERGE_PROPERTY(writing_mode); - MERGE_PROPERTY(opacity); - MERGE_PROPERTY(letter_spacing); - MERGE_PROPERTY(ruby); -#undef MERGE_PROPERTY + 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; @@ -39,7 +54,6 @@ StyleProperties inherited_style(const StyleProperties& parent) { result.text_decoration = parent.text_decoration; result.text_outline = parent.text_outline; result.writing_mode = parent.writing_mode; - result.opacity = parent.opacity; result.letter_spacing = parent.letter_spacing; result.ruby = parent.ruby; return result; @@ -47,17 +61,15 @@ StyleProperties inherited_style(const StyleProperties& parent) { const StyleProperties& initial_style() { static const StyleProperties style{ - .background_color = StyleColorValue{ { 0, 0, 0, 255 } }, + .background_color = StyleColorValue{ { 0, 0, 0, 0 } }, .color = StyleColorValue{ { 255, 255, 255, 255 } }, .font_family = "round gothic", - .font_size = StyleFontSizeValue{ 64.0, 64.0 }, .font_style = StyleFontStyleValue::Normal, .font_weight = StyleFontWeightValue::Normal, - .line_height = 64.0, + .line_height = NormalLineHeight{}, .text_decoration = StyleTextDecorationValue::None, .text_outline = None{}, .writing_mode = StyleWritingModeValue::Lrtb, - .opacity = 1.0, .letter_spacing = 0.0, }; return style; @@ -65,168 +77,230 @@ const StyleProperties& initial_style() { void complete_style(StyleProperties& style) { const auto& initial = initial_style(); -#define APPLY_INITIAL(name) if (!style.name) style.name = initial.name - APPLY_INITIAL(background_color); - APPLY_INITIAL(color); - APPLY_INITIAL(font_family); - APPLY_INITIAL(font_size); - APPLY_INITIAL(font_style); - APPLY_INITIAL(font_weight); - APPLY_INITIAL(line_height); - APPLY_INITIAL(text_decoration); - APPLY_INITIAL(text_outline); - APPLY_INITIAL(writing_mode); - APPLY_INITIAL(opacity); - APPLY_INITIAL(letter_spacing); -#undef APPLY_INITIAL + 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); } -resolved::Timing resolve_timing(const ast::Timing& timing, const resolved::Timing& parent) { - resolved::Timing result; - result.begin = timing.begin +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; - result.end = timing.end + resolved_timing.end = timing.end ? std::optional{parent.begin.value_or(std::chrono::milliseconds{}) + *timing.end} : parent.end; - return result; + + 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: - explicit Resolver(const ast::Document& document) : document_(document) { - for (const auto& style : document.styles) styles_.emplace(style.id, &style); - for (const auto& region : document.regions) regions_.emplace(region.id, ®ion); + 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; - for (const auto& division : document_.divisions) { - resolved::Division resolved_division; - resolved_division.timing = resolve_timing(division.timing, {}); + if (!document_.division) { + return { + .document = std::move(output) + }; + } - for (const auto& paragraph : division.paragraphs) { - resolved::Paragraph resolved_paragraph; - resolved_paragraph.id = paragraph.id; - resolved_paragraph.timing = resolve_timing(paragraph.timing, resolved_division.timing); + const auto& division = *document_.division; + auto division_timing = resolve_timing(division.timing, {}, mode_); + if (division_timing.has_error) { + return { + .document = std::move(output) + }; + } - if (!apply_refs(resolved_paragraph.style, paragraph.style_refs)) { - return failure(); - } + resolved::Division resolved_division; + resolved_division.timing = std::move(division_timing.timing); + resolved_division.paragraphs.reserve(division.paragraphs.size()); - merge_style(resolved_paragraph.style, paragraph.inline_style); - if (!resolve_region(paragraph.region, resolved_paragraph.region)) { - return failure(); - } + 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 (resolved_paragraph.region) { - auto region_style = inherited_style(resolved_paragraph.region->style); - merge_style(region_style, resolved_paragraph.style); - resolved_paragraph.style = std::move(region_style); + 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; } - for (const auto& span : paragraph.spans) { - resolved::Span resolved_span; - resolved_span.id = span.id; - resolved_span.timing = resolve_timing(span.timing, resolved_paragraph.timing); - resolved_span.style = inherited_style(resolved_paragraph.style); - if (!apply_refs(resolved_span.style, span.style_refs)) { - return failure(); - } - - merge_style(resolved_span.style, span.inline_style); - if (!resolve_region(span.region, resolved_span.region)) { - return failure(); - } - - if (resolved_span.region) { - auto region_style = inherited_style(resolved_span.region->style); - merge_style(region_style, resolved_span.style); - resolved_span.style = std::move(region_style); - } - complete_style(resolved_span.style); - resolved_span.content = span.content; - resolved_paragraph.spans.push_back(std::move(resolved_span)); + 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) + }; } - resolved_division.paragraphs.push_back(std::move(resolved_paragraph)); + 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)); } - output.divisions.push_back(std::move(resolved_division)); + + resolved_division.paragraphs.push_back(std::move(resolved_paragraph)); } + + output.division = std::move(resolved_division); return { .document = std::move(output), - .error = std::nullopt + .error = {} }; } private: - bool apply_refs(StyleProperties& target, const std::vector& refs) { + std::optional apply_refs(StyleProperties& target, const std::vector& refs) { for (const auto& ref : refs) { StyleProperties resolved; - if (!resolve_style(ref, resolved)) return false; + if (auto error = resolve_style(ref, resolved)) { + return error; + } + merge_style(target, resolved); } - return true; + return {}; } - bool resolve_style(const std::string& id, StyleProperties& output) { + 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 true; + return {}; } const auto found = styles_.find(id); if (found == styles_.end()) { - error_ = ResolveError{ "unknown style reference: " + id }; - return false; + return ResolveError{ "unknown style reference: " + std::string{id} }; } if (!resolving_styles_.emplace(id).second) { - error_ = ResolveError{ "cyclic style reference: " + id }; - return false; + return ResolveError{ "cyclic style reference: " + std::string{id} }; } StyleProperties resolved; - if (!apply_refs(resolved, found->second->style_refs)) { - return false; + 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 true; + return {}; } - bool resolve_region(const std::optional& id, std::optional& output) { - if (!id) return true; + std::optional resolve_region( + const std::optional& id, + std::optional& output) { + if (!id) return {}; const auto found = regions_.find(*id); if (found == regions_.end()) { - error_ = ResolveError{ "unknown region reference: " + *id }; - return false; + return ResolveError{ "unknown region reference: " + *id }; } StyleProperties style; - if (!apply_refs(style, found->second->style_refs)) { - return false; + 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 true; - } - - ResolveResult failure() { - return { .document = std::nullopt, .error = std::move(error_) }; + return {}; } const ast::Document& document_; - std::unordered_map styles_; - std::unordered_map regions_; - std::unordered_map resolved_styles_; - std::unordered_set resolving_styles_; - std::optional error_; + 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) { - return Resolver{document}.run(); +ResolveResult resolve(const ast::Document& document, SyncMode mode) { + return Resolver{document, mode}.run(); } -} // namespace arib::ttml +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/resolver.h b/src/ttml/resolver.h index 9ffd1a1..6ae4f91 100644 --- a/src/ttml/resolver.h +++ b/src/ttml/resolver.h @@ -2,11 +2,14 @@ #include "ttml/ast.h" #include "ttml/resolved.h" +#include "ttml/sync_mode.h" #include #include -namespace arib::ttml { +namespace arib { + +namespace ttml { struct ResolveError { std::string message; @@ -16,11 +19,13 @@ struct ResolveResult { std::optional document; std::optional error; - bool has_error() const { + [[nodiscard]] bool has_error() const noexcept { return error.has_value(); } }; -ResolveResult resolve(const ast::Document& document); +[[nodiscard]] ResolveResult resolve(const ast::Document& document, SyncMode mode); + +} // namespace ttml -} // namespace arib::ttml +} // namespace arib diff --git a/src/ttml/string_utils.h b/src/ttml/string_utils.h index 67f7ff4..486ab08 100644 --- a/src/ttml/string_utils.h +++ b/src/ttml/string_utils.h @@ -1,13 +1,35 @@ #pragma once +#include +#include +#include #include #include -namespace arib::ttml { +namespace arib { + +namespace ttml { constexpr std::string_view kWhitespace = " \t\r\n"; -inline std::vector split_by_whitespace(std::string_view value) { +[[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); @@ -25,4 +47,6 @@ inline std::vector split_by_whitespace(std::string_view value) return result; } -} // namespace arib::ttml +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/style.cpp b/src/ttml/style.cpp index d5c2d50..be15c92 100644 --- a/src/ttml/style.cpp +++ b/src/ttml/style.cpp @@ -1,7 +1,7 @@ -#include "ttml/style.h" -#include "ttml/string_utils.h" -#include -#include +#include "ttml/style.h" +#include "ttml/string_utils.h" +#include +#include namespace arib { @@ -9,8 +9,8 @@ namespace ttml { namespace { -bool parse_hex_bytes(std::string_view input, std::span output) { - if (input.size() > output.size() * 2) { +bool hex_to_bytes(std::string_view input, std::span output) { + if (input.size() % 2 != 0 || input.size() / 2 > output.size()) { return false; } @@ -21,7 +21,8 @@ bool parse_hex_bytes(std::string_view input, std::span output) { return -1; }; - for (size_t i = 0; i < output.size(); ++i) { + 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]); @@ -47,22 +48,23 @@ std::optional parse_color(std::string_view color_string) { } StyleColorValue color; - - // #RRGGBB if (color_string.length() == 7) { - if (!parse_hex_bytes(color_string.substr(1), color.rgba)) { + // #RRGGBB + if (!hex_to_bytes(color_string.substr(1), color.rgba)) { return {}; } color.rgba[3] = 0xFF; } - - // #RRGGBBAA - if (color_string.length() == 9) { - if (!parse_hex_bytes(color_string.substr(1), color.rgba)) { + else if (color_string.length() == 9) { + // #RRGGBBAA + if (!hex_to_bytes(color_string.substr(1), color.rgba)) { return {}; } } + else { + return {}; + } return color; } @@ -74,24 +76,10 @@ std::optional parse_length(std::string_view value) { } value.remove_suffix(suffix.size()); - - if (value.empty()) { - return {}; - } - - std::string temp{ value }; - - char* end = nullptr; - const double result = std::strtod(temp.c_str(), &end); - - if (end != temp.c_str() + temp.size()) { - return {}; - } - - return result; + return string_to_double(value); } -std::optional parse_length_pair(std::string_view length_string) { +std::optional parse_length_pair(std::string_view length_string, SingleLengthMode single_length_mode) { if (length_string.empty()) { return {}; } @@ -101,31 +89,32 @@ std::optional parse_length_pair(std::string_view length_string) { return {}; } - const auto split = split_by_whitespace(length_string); + const auto split = split_by_whitespace(length_string); if (split.empty() || split.size() > 2) { return {}; } - const auto width = parse_length(split[0]); - if (!width) { + const auto x = parse_length(split[0]); + if (!x) { return {}; } if (split.size() == 1) { - return LengthPair{ *width, *width }; + if (single_length_mode == SingleLengthMode::Reject) { + return {}; + } + return LengthPair{ *x, *x }; } - const auto height = parse_length(split[1]); - if (!height) { + const auto y = parse_length(split[1]); + if (!y) { return {}; } - return LengthPair{ *width, *height }; + return LengthPair{ *x, *y }; } std::optional parse_text_outline(std::string_view value_string) { - TextOutline text_outline; - if (value_string.empty()) { return {}; } @@ -139,70 +128,39 @@ std::optional parse_text_outline(std::string_view value_s return {}; } - const auto split = split_by_whitespace(value_string); - if (split.size() == 1) { - const auto border_width = parse_length(split[0]); - if (!border_width) { - return {}; - } - - text_outline.border_width = *border_width; - return text_outline; + const auto split = split_by_whitespace(value_string); + if (split.empty() || split.size() > 3) { + return {}; } - else if (split.size() == 2) { - const auto color = parse_color(split[0]); - if (color) { - text_outline.color = *color; - - const auto border_width = parse_length(split[1]); - if (!border_width) { - return {}; - } - - text_outline.border_width = *border_width; - } - else { - const auto border_width = parse_length(split[0]); - if (!border_width) { - return {}; - } - text_outline.border_width = *border_width; - - const auto blur_width = parse_length(split[1]); - if (!blur_width) { - return {}; - } - - text_outline.blur_width = *blur_width; - } - return text_outline; - } - else if (split.size() == 3) { - const auto color = parse_color(split[0]); - if (!color) { - 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 border_width = parse_length(split[1]); - if (!border_width) { - return {}; - } + const auto length_count = split.size() - length_index; + if (length_count == 0 || length_count > 2) { + return {}; + } - text_outline.border_width = *border_width; + const auto border_width = parse_length(split[length_index]); + if (!border_width) { + return {}; + } + text_outline.border_width = *border_width; - const auto blur_width = parse_length(split[2]); + 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; } - return {}; + return text_outline; } } // namespace ttml -} // namespace arib +} // namespace arib diff --git a/src/ttml/style.h b/src/ttml/style.h index 83762a8..9989a44 100644 --- a/src/ttml/style.h +++ b/src/ttml/style.h @@ -1,7 +1,9 @@ #pragma once +#include +#include #include #include -#include +#include #include namespace arib { @@ -13,23 +15,25 @@ struct None {}; struct StyleColorValue { std::array rgba; - constexpr std::uint8_t r() const { return rgba[0]; } - constexpr std::uint8_t g() const { return rgba[1]; } - constexpr std::uint8_t b() const { return rgba[2]; } - constexpr std::uint8_t a() const { return rgba[3]; } + 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 width{}; - double height{}; -}; +struct LengthPair { + double x{}; + double y{}; +}; -using StyleExtentValue = LengthPair; -using StyleFontSizeValue = LengthPair; +enum class SingleLengthMode { + Reject, + Repeat, +}; -enum class StyleFontStyleValue { +enum class StyleFontStyleValue { Normal, Italic }; @@ -63,27 +67,25 @@ enum class StyleWritingModeValue { struct StyleProperties { std::optional background_color; std::optional color; - std::optional extent; + std::optional extent; std::optional font_family; - std::optional font_size; + std::optional font_size; std::optional font_style; std::optional font_weight; std::optional line_height; - std::optional origin; + std::optional origin; std::optional text_decoration; std::optional text_outline; std::optional writing_mode; - std::optional opacity; std::optional letter_spacing; std::optional ruby; - }; -std::optional parse_color(std::string_view text); -std::optional parse_length(std::string_view value); -std::optional parse_length_pair(std::string_view length_string); -std::optional parse_text_outline(std::string_view value_string); +[[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 \ No newline at end of file +} // namespace arib diff --git a/src/ttml/sync_mode.h b/src/ttml/sync_mode.h new file mode 100644 index 0000000..4b02fce --- /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 From ed44f93f3feb4c2988f4359681debb703eff4ecb Mon Sep 17 00:00:00 2001 From: nekohkr Date: Wed, 19 Aug 2026 05:30:12 +0900 Subject: [PATCH 04/11] Improve ARIB caption encoding compatibility --- msvc/dantto4k.vcxproj | 4 +- msvc/dantto4k.vcxproj.filters | 8 +- msvc/dantto4k_dll.vcxproj | 16 +- msvc/dantto4k_dll.vcxproj.filters | 55 ++++- src/aribCharsetEncoder.cpp | 68 +++-- src/b24SubtitleConverter.h | 1 - src/ttml.cpp | 397 ------------------------------ src/ttml.h | 154 ------------ src/ttml/b24_converter.cpp | 14 +- 9 files changed, 115 insertions(+), 602 deletions(-) delete mode 100644 src/ttml.cpp delete mode 100644 src/ttml.h diff --git a/msvc/dantto4k.vcxproj b/msvc/dantto4k.vcxproj index 6f18160..fcf64e7 100644 --- a/msvc/dantto4k.vcxproj +++ b/msvc/dantto4k.vcxproj @@ -200,7 +200,6 @@ - @@ -317,7 +316,6 @@ - @@ -390,4 +388,4 @@ - + \ No newline at end of file diff --git a/msvc/dantto4k.vcxproj.filters b/msvc/dantto4k.vcxproj.filters index 71d5949..4622590 100644 --- a/msvc/dantto4k.vcxproj.filters +++ b/msvc/dantto4k.vcxproj.filters @@ -250,9 +250,6 @@ dantto4k\subtitle - - dantto4k\subtitle - dantto4k @@ -609,9 +606,6 @@ dantto4k\subtitle - - dantto4k\subtitle - dantto4k\subtitle @@ -774,4 +768,4 @@ {84aaa3a8-9e4a-4085-9dc5-db67d16195cc} - + \ No newline at end of file diff --git a/msvc/dantto4k_dll.vcxproj b/msvc/dantto4k_dll.vcxproj index 7566d5f..401bc01 100644 --- a/msvc/dantto4k_dll.vcxproj +++ b/msvc/dantto4k_dll.vcxproj @@ -130,6 +130,7 @@ stdcpp20 MultiThreadedDebugDLL /FS %(AdditionalOptions) + $(ProjectDir)../src;%(AdditionalIncludeDirectories) Windows @@ -149,6 +150,7 @@ stdcpp20 MultiThreadedDLL /FS %(AdditionalOptions) + $(ProjectDir)../src;%(AdditionalIncludeDirectories) Windows @@ -213,7 +215,6 @@ - @@ -263,6 +264,10 @@ + + + + @@ -326,7 +331,6 @@ - @@ -387,6 +391,14 @@ + + + + + + + + diff --git a/msvc/dantto4k_dll.vcxproj.filters b/msvc/dantto4k_dll.vcxproj.filters index 6cae4f5..cca5905 100644 --- a/msvc/dantto4k_dll.vcxproj.filters +++ b/msvc/dantto4k_dll.vcxproj.filters @@ -250,9 +250,6 @@ dantto4k\subtitle - - dantto4k\subtitle - dantto4k @@ -304,12 +301,24 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + @@ -597,9 +606,6 @@ dantto4k\subtitle - - dantto4k\subtitle - dantto4k\subtitle @@ -672,12 +678,36 @@ dantto4k - + dantto4k - + dantto4k + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + + + dantto4k\ttml + @@ -734,5 +764,8 @@ {5dccb4d7-560f-4244-9d84-334de3cf19f2} + + {e31c2a9a-285e-49f0-bf5f-da6ccd870531} + - + \ No newline at end of file diff --git a/src/aribCharsetEncoder.cpp b/src/aribCharsetEncoder.cpp index 7b475a8..87aa4d2 100644 --- a/src/aribCharsetEncoder.cpp +++ b/src/aribCharsetEncoder.cpp @@ -73,7 +73,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,7 +82,7 @@ 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) { @@ -90,44 +90,58 @@ std::optional findChar(char32_t c, CharsetCode candidate1, Cha } 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]; } -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 +161,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 +250,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 +307,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 +320,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; } @@ -420,13 +435,18 @@ class Encoder { } } else { + CharsetCode charsetCode = charset->code; + if (mode == EncodeMode::Caption && charsetCode == CharsetCode::JISKanjiPlane1) { + charsetCode = CharsetCode::Kanji; + } + // charset is not in G0-G3 if (charset->code == CharsetCode::JISX0201Katakana) { - setGraphicToCharset(1, charset->code); + setGraphicToCharset(1, charsetCode); setGR(1); } else { - setGraphicToCharset(0, charset->code); + setGraphicToCharset(0, charsetCode); setGL(0); } } @@ -519,7 +539,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 +556,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; } diff --git a/src/b24SubtitleConverter.h b/src/b24SubtitleConverter.h index fa35851..3f18ecd 100644 --- a/src/b24SubtitleConverter.h +++ b/src/b24SubtitleConverter.h @@ -7,7 +7,6 @@ #include #include #include -#include "ttml.h" namespace B24 { enum class DataUnitParameter : uint8_t { diff --git a/src/ttml.cpp b/src/ttml.cpp deleted file mode 100644 index e2b514d..0000000 --- a/src/ttml.cpp +++ /dev/null @@ -1,397 +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; -} - -void mergeStyle(TTMLStyle& target, const TTMLStyle& source) { - if (source.fontSize) target.fontSize = source.fontSize; - if (source.lineHeight) target.lineHeight = source.lineHeight; - if (source.letterSpacing) target.letterSpacing = source.letterSpacing; - if (source.fontWeight) target.fontWeight = source.fontWeight; - if (source.fontStyle) target.fontStyle = source.fontStyle; - if (source.color) target.color = source.color; - if (source.backgroundColor) target.backgroundColor = source.backgroundColor; - if (source.outlineSpecified) { - target.outlineSpecified = true; - target.outlineColor = source.outlineColor; - } - if (source.fontFamily) target.fontFamily = source.fontFamily; - if (source.textDecoration) target.textDecoration = source.textDecoration; - if (source.writingMode) target.writingMode = source.writingMode; - if (source.opacity) target.opacity = source.opacity; - if (source.border) target.border = source.border; - if (source.animation) target.animation = source.animation; - if (source.ruby) target.ruby = source.ruby; - if (source.textShadow) target.textShadow = source.textShadow; - if (source.overflow) target.overflow = source.overflow; -} - -TTMLStyle inheritedStyle(const TTMLStyle& parent) { - TTMLStyle result; - result.fontSize = parent.fontSize; - result.lineHeight = parent.lineHeight; - result.letterSpacing = parent.letterSpacing; - result.fontWeight = parent.fontWeight; - result.fontStyle = parent.fontStyle; - result.color = parent.color; - result.outlineSpecified = parent.outlineSpecified; - result.outlineColor = parent.outlineColor; - result.fontFamily = parent.fontFamily; - result.textDecoration = parent.textDecoration; - result.writingMode = parent.writingMode; - result.ruby = parent.ruby; - result.textShadow = parent.textShadow; - // tts:backgroundColor is not inherited in TTML. - return result; -} - -TTMLStyle initialStyle() { - TTMLStyle result; - // TR-B39 has no initial fontSize. Keep it absent so callers can - // distinguish an invalid/incomplete document from a 144px style. - result.lineHeight = TtmlStyleValue(TtmlStyleValueKeyword("normal")); - result.letterSpacing = TtmlStyleValue(TtmlStyleValueLength(0.0f, "px")); - result.fontWeight = TtmlStyleValue(TtmlStyleValueKeyword("normal")); - result.fontStyle = TtmlStyleValue(TtmlStyleValueKeyword("normal")); - result.color = TtmlStyleValue(TtmlStyleValueColor(255, 255, 255, 255)); - result.backgroundColor = TtmlStyleValue(TtmlStyleValueColor(0, 0, 0, 0)); - result.outlineSpecified = true; - result.fontFamily = "round gothic"; - result.textDecoration = TtmlStyleValue(TtmlStyleValueKeyword("none")); - result.writingMode = TtmlStyleValue(TtmlStyleValueKeyword("lrtb")); - result.opacity = TtmlStyleValue(TtmlStyleValueNumber(1.0f)); - result.border = "none 0px #00000000"; - result.animation = TtmlStyleValue(TtmlStyleValueKeyword("none")); - result.ruby = TtmlStyleValue(TtmlStyleValueKeyword("none")); - result.textShadow = "none"; - result.overflow = TtmlStyleValue(TtmlStyleValueKeyword("hidden")); - // textOutline initial value is "none", represented by no color. - return result; -} - -void completeComputedStyle(TTMLStyle& style) { - const TTMLStyle defaults = initialStyle(); - if (!style.lineHeight) style.lineHeight = defaults.lineHeight; - if (!style.letterSpacing) style.letterSpacing = defaults.letterSpacing; - if (!style.fontWeight) style.fontWeight = defaults.fontWeight; - if (!style.fontStyle) style.fontStyle = defaults.fontStyle; - if (!style.color) style.color = defaults.color; - if (!style.backgroundColor) style.backgroundColor = defaults.backgroundColor; - if (!style.fontFamily) style.fontFamily = defaults.fontFamily; - if (!style.textDecoration) style.textDecoration = defaults.textDecoration; - if (!style.writingMode) style.writingMode = defaults.writingMode; - if (!style.opacity) style.opacity = defaults.opacity; - if (!style.border) style.border = defaults.border; - if (!style.animation) style.animation = defaults.animation; - if (!style.ruby) style.ruby = defaults.ruby; - if (!style.textShadow) style.textShadow = defaults.textShadow; - if (!style.overflow) style.overflow = defaults.overflow; -} - -void applyTextOutline(const pugi::xml_attribute& attribute, TTMLStyle& target) { - if (!attribute) { - return; - } - - target.outlineSpecified = true; - target.outlineColor.reset(); - std::istringstream valueStream(attribute.value()); - std::string color; - valueStream >> color; - if (color == "none") { - return; - } - if (!color.empty() && color.front() == '#') { - target.outlineColor = TtmlStyleValueParser::parse(color); - } -} - -void applyReferencedStyles(const pugi::xml_node& node, const std::list& styles, TTMLStyle& target) { - std::istringstream styleStream(node.attribute("style").value()); - std::string styleId; - while (styleStream >> styleId) { - const auto style = std::find_if(styles.begin(), styles.end(), [&styleId](const TTMLStyle& candidate) { - return candidate.id == styleId; - }); - if (style != styles.end()) { - mergeStyle(target, *style); - } - } -} - -void applyInlineStyles(const pugi::xml_node& node, TTMLStyle& target) { - if (node.attribute("tts:fontSize")) { - target.fontSize = TtmlStyleValueParser::parseFontSize(node.attribute("tts:fontSize").value()); - } - if (node.attribute("tts:lineHeight")) { - target.lineHeight = TtmlStyleValueParser::parse(node.attribute("tts:lineHeight").value()); - } - if (node.attribute("arib-tt:letter-spacing")) { - target.letterSpacing = TtmlStyleValueParser::parse(node.attribute("arib-tt:letter-spacing").value()); - } - if (node.attribute("tts:fontWeight")) { - target.fontWeight = TtmlStyleValueParser::parse(node.attribute("tts:fontWeight").value()); - } - if (node.attribute("tts:fontStyle")) { - target.fontStyle = TtmlStyleValueParser::parse(node.attribute("tts:fontStyle").value()); - } - if (node.attribute("tts:color")) { - target.color = TtmlStyleValueParser::parse(node.attribute("tts:color").value()); - } - if (node.attribute("tts:backgroundColor")) { - target.backgroundColor = TtmlStyleValueParser::parse(node.attribute("tts:backgroundColor").value()); - } - if (node.attribute("tts:fontFamily")) { - target.fontFamily = node.attribute("tts:fontFamily").value(); - } - if (node.attribute("tts:textDecoration")) { - target.textDecoration = TtmlStyleValueParser::parse(node.attribute("tts:textDecoration").value()); - } - if (node.attribute("tts:writingMode")) { - target.writingMode = TtmlStyleValueParser::parse(node.attribute("tts:writingMode").value()); - } - if (node.attribute("tts:opacity")) { - target.opacity = TtmlStyleValueParser::parse(node.attribute("tts:opacity").value()); - } - if (node.attribute("arib-tt:border")) { - target.border = node.attribute("arib-tt:border").value(); - } - if (node.attribute("arib-tt:animation")) { - target.animation = TtmlStyleValueParser::parse(node.attribute("arib-tt:animation").value()); - } - if (node.attribute("arib-tt:ruby")) { - target.ruby = TtmlStyleValueParser::parse(node.attribute("arib-tt:ruby").value()); - } - if (node.attribute("arib-tt:text-shadow")) { - target.textShadow = node.attribute("arib-tt:text-shadow").value(); - } - if (node.attribute("tts:overflow")) { - target.overflow = TtmlStyleValueParser::parse(node.attribute("tts:overflow").value()); - } - applyTextOutline(node.attribute("tts:textOutline"), target); -} - -} - -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")) { - const auto [width, height] = TtmlStyleValueParser::parseLengthPair(p.attribute("tts:extent").value()); - region.extent = TtmlStyleExtent{ width, height }; - } - if (p.attribute("tts:origin")) { - const auto [x, y] = TtmlStyleValueParser::parseLengthPair(p.attribute("tts:origin").value()); - region.origin = TtmlStyleOrigin{ x, y }; - } - 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(); - applyInlineStyles(p, style); - - output.styles.push_back(style); - } - - // A style element may itself reference one or more styles. Resolve those - // references before content styles are computed; the element's own values - // have higher priority than its referenced styles. - for (pugi::xml_node node : doc.child("tt").child("head").child("styling").children("style")) { - const std::string id = node.attribute("xml:id").value(); - auto own = std::find_if(output.styles.begin(), output.styles.end(), [&id](const TTMLStyle& style) { - return style.id == id; - }); - if (own == output.styles.end() || !node.attribute("style")) { - continue; - } - TTMLStyle resolved; - applyReferencedStyles(node, output.styles, resolved); - mergeStyle(resolved, *own); - resolved.id = own->id; - *own = resolved; - } - - const pugi::xml_node body = doc.child("tt").child("body"); - TTMLStyle bodyStyle = initialStyle(); - applyReferencedStyles(body, output.styles, bodyStyle); - applyInlineStyles(body, bodyStyle); - - for (pugi::xml_node div : body.children("div")) { - TTMLDivTag divTag; - TTMLStyle divStyle = inheritedStyle(bodyStyle); - applyReferencedStyles(div, output.styles, divStyle); - applyInlineStyles(div, divStyle); - 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; - TTMLStyle pStyle = inheritedStyle(divStyle); - applyReferencedStyles(p, output.styles, pStyle); - applyInlineStyles(p, pStyle); - 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(); - spanTag.style = inheritedStyle(pStyle); - applyReferencedStyles(span, output.styles, spanTag.style); - applyInlineStyles(span, spanTag.style); - completeComputedStyle(spanTag.style); - - pTag.spanTags.push_back(spanTag); - } - - if (pTag.spanTags.empty()) { - const std::string text = p.text().get(); - if (!text.empty()) { - TTMLSpanTag spanTag; - spanTag.text = text; - spanTag.style = inheritedStyle(pStyle); - completeComputedStyle(spanTag.style); - pTag.spanTags.push_back(std::move(spanTag)); - } - } - - divTag.pTags.push_back(pTag); - } - - output.divTags.push_back(divTag); - } - - return output; -} - -TtmlStyleValueColor TtmlStyleValueColor::parse(std::string_view text) { - // ARIB TR-B39 8.5.6.4 operates only #RRGGBB and #RRGGBBAA. - if ((text.size() != 7 && text.size() != 9) || text.front() != '#') { - throw std::invalid_argument("Invalid ARIB-TTML color. Expected #RRGGBB or #RRGGBBAA"); - } - - const unsigned long value = std::stoul(std::string(text.substr(1)), nullptr, 16); - const uint8_t alpha = text.size() == 7 ? 0xFF : static_cast(value & 0xFF); - const uint8_t blue = text.size() == 7 ? static_cast(value & 0xFF) : static_cast((value >> 8) & 0xFF); - const uint8_t green = text.size() == 7 ? static_cast((value >> 8) & 0xFF) : static_cast((value >> 16) & 0xFF); - const uint8_t red = text.size() == 7 ? static_cast((value >> 16) & 0xFF) : static_cast((value >> 24) & 0xFF); - return { red, green, blue, alpha }; -} - -TtmlStyleValue TtmlStyleValueParser::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 TtmlStyleValue(TtmlStyleValueLength(std::stof(match[1]), match[2])); - } - else { - throw std::invalid_argument("Invalid length value: " + input); - } - } - - if (input.find("#") == 0) { - return TtmlStyleValue(TtmlStyleValueColor::parse(input)); - } - - static const std::set validKeywords = { - "bold", "italic", "normal", "none", "underline", "lrtb", "tbrl", - "hidden", "visible", "ruby", "emphasis", "base", "text", "container" - }; - if (validKeywords.find(input) != validKeywords.end()) { - return TtmlStyleValue(TtmlStyleValueKeyword(input)); - } - - return TtmlStyleValue(TtmlStyleValueNumber(std::stof(input))); - - } - catch (const std::invalid_argument& e) { - std::cerr << e.what() << std::endl; - throw; - } -} - -std::pair TtmlStyleValueParser::parseLengthPair(const std::string& input) { - std::istringstream iss(input); - std::string token1, token2; - if (!(iss >> token1)) { - throw std::invalid_argument("Failed to parse TTML value pair from: " + input); - } - if (!(iss >> token2)) { - token2 = token1; - } - const TtmlStyleValue value1 = parse(token1); - const TtmlStyleValue value2 = parse(token2); - if (!value1.isValue() || !value2.isValue()) { - throw std::invalid_argument("Expected TTML length pair: " + input); - } - return std::make_pair(value1.getValue(), value2.getValue()); -} - -TtmlStyleFontSize TtmlStyleValueParser::parseFontSize(const std::string& input) { - const auto [width, height] = parseLengthPair(input); - return { width, height }; -} diff --git a/src/ttml.h b/src/ttml.h deleted file mode 100644 index 6178b6f..0000000 --- a/src/ttml.h +++ /dev/null @@ -1,154 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct TtmlStyleValueLength { - TtmlStyleValueLength(float v, const std::string& u) : value(v), unit(u) {} - - float value; - std::string unit; - -}; - -struct TtmlStyleValueColor { - TtmlStyleValueColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : r(r), g(g), b(b), a(a) {} - - static TtmlStyleValueColor parse(std::string_view text); - - uint8_t r; - uint8_t g; - uint8_t b; - uint8_t a; -}; - -struct TtmlStyleValueKeyword { - TtmlStyleValueKeyword(const std::string& k) : keyword(k) {} - - std::string keyword; -}; - -struct TtmlStyleValueNumber { - TtmlStyleValueNumber(float n) : number(n) {} - - float number; -}; - -class TtmlStyleValue { -public: - using ValueType = std::variant; - -private: - ValueType value; - -public: - TtmlStyleValue(TtmlStyleValueLength length) : value(length) {} - TtmlStyleValue(TtmlStyleValueColor color) : value(color) {} - TtmlStyleValue(TtmlStyleValueKeyword keyword) : value(keyword) {} - TtmlStyleValue(TtmlStyleValueNumber number) : value(number) {} - - template - T getValue() const { - return std::get(value); - } - - template - bool isValue() const { - return std::holds_alternative(value); - } - - template - void setValue(const T& new_value) { - value = new_value; - } -}; - -struct TtmlStyleFontSize { - TtmlStyleValueLength width; - TtmlStyleValueLength height; -}; - -struct TtmlStyleExtent { - TtmlStyleValueLength width; - TtmlStyleValueLength height; -}; - -struct TtmlStyleOrigin { - TtmlStyleValueLength x; - TtmlStyleValueLength y; -}; - -class TtmlStyleValueParser { -public: - static TtmlStyleValue parse(const std::string& input); - static TtmlStyleFontSize parseFontSize(const std::string& input); - static std::pair parseLengthPair(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 letterSpacing; - std::optional fontWeight; - std::optional fontStyle; - std::optional color; - std::optional backgroundColor; - bool outlineSpecified = false; - std::optional outlineColor; - std::optional fontFamily; - std::optional textDecoration; - std::optional writingMode; - std::optional opacity; - std::optional border; - std::optional animation; - std::optional ruby; - std::optional textShadow; - std::optional overflow; -}; - -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); - -}; diff --git a/src/ttml/b24_converter.cpp b/src/ttml/b24_converter.cpp index e52d10d..a974dd6 100644 --- a/src/ttml/b24_converter.cpp +++ b/src/ttml/b24_converter.cpp @@ -472,10 +472,18 @@ class Encoder { 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::max(1.0, std::round(font_width * kLayoutScale))); - uint32_t composition_height = static_cast(std::max(1.0, std::round(font_height * kLayoutScale))); + uint32_t composition_width = 36; + uint32_t composition_height = 36; + uint8_t character_size = B24ControlSet::NSZ; + if (font_width == 72 && font_height == 144) { + character_size = B24ControlSet::MSZ; + } + else { + composition_width = static_cast(std::max(1.0, std::round(font_width * kLayoutScale))); + composition_height = static_cast(std::max(1.0, std::round(font_height * kLayoutScale))); + } set_character_composition(composition_width, composition_height); - set_character_size(B24ControlSet::NSZ); + set_character_size(character_size); } if (style.color) { From 8eaab44aed269937c5fc557e6c6fb97bbb5855ed Mon Sep 17 00:00:00 2001 From: nekohkr Date: Wed, 19 Aug 2026 05:51:51 +0900 Subject: [PATCH 05/11] Fix build error --- msvc/dantto4k_dll.vcxproj | 4 ++-- msvc/dantto4k_dll.vcxproj.filters | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/msvc/dantto4k_dll.vcxproj b/msvc/dantto4k_dll.vcxproj index 401bc01..0bf33a1 100644 --- a/msvc/dantto4k_dll.vcxproj +++ b/msvc/dantto4k_dll.vcxproj @@ -182,7 +182,6 @@ - @@ -264,6 +263,7 @@ + @@ -291,7 +291,6 @@ - @@ -391,6 +390,7 @@ + diff --git a/msvc/dantto4k_dll.vcxproj.filters b/msvc/dantto4k_dll.vcxproj.filters index cca5905..eb5abc5 100644 --- a/msvc/dantto4k_dll.vcxproj.filters +++ b/msvc/dantto4k_dll.vcxproj.filters @@ -244,9 +244,6 @@ dantto4k\xml - - dantto4k\subtitle - dantto4k\subtitle @@ -319,6 +316,9 @@ dantto4k\ttml + + dantto4k\subtitle + @@ -603,9 +603,6 @@ dantto4k\xml - - dantto4k\subtitle - dantto4k\subtitle @@ -708,6 +705,9 @@ dantto4k\ttml + + dantto4k\subtitle + From 9743e6401f3ade033ea71e8a7353def40d25b031 Mon Sep 17 00:00:00 2001 From: nekohkr Date: Wed, 19 Aug 2026 05:55:08 +0900 Subject: [PATCH 06/11] Fix build error --- Makefile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4819893..5842f0e 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,10 @@ 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)) +SRC_FILES = $(filter-out $(SRC_DIR)/bonTuner.cpp $(SRC_DIR)/dllmain.cpp, \ + $(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(SRC_DIR)/ttml/*.cpp)) -OBJ_FILES = $(SRC_FILES:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o) +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) @@ -14,7 +15,7 @@ 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 +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) @@ -28,6 +29,7 @@ $(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: @@ -36,4 +38,4 @@ clean: install: cp $(EXEC) /usr/local/bin/$(PROJECT_NAME) -.PHONY: all clean install \ No newline at end of file +.PHONY: all clean install From 2d113d37966d9b4903e825a5b4eb0c4cacde6b8e Mon Sep 17 00:00:00 2001 From: nekohkr Date: Wed, 19 Aug 2026 11:46:41 +0900 Subject: [PATCH 07/11] Fix build error --- msvc/dantto4k.vcxproj | 2 +- msvc/dantto4k_dll.vcxproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/msvc/dantto4k.vcxproj b/msvc/dantto4k.vcxproj index fcf64e7..16eee5e 100644 --- a/msvc/dantto4k.vcxproj +++ b/msvc/dantto4k.vcxproj @@ -134,7 +134,7 @@ _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 - MultiThreadedDLL + MultiThreaded /FS %(AdditionalOptions) $(ProjectDir)../src;%(AdditionalIncludeDirectories) diff --git a/msvc/dantto4k_dll.vcxproj b/msvc/dantto4k_dll.vcxproj index 0bf33a1..90e0d87 100644 --- a/msvc/dantto4k_dll.vcxproj +++ b/msvc/dantto4k_dll.vcxproj @@ -148,7 +148,7 @@ _CRT_SECURE_NO_WARNINGS;TSDUCK_STATIC_LIBRARY;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;NDEBUG;DANTTO4KDLL_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) true stdcpp20 - MultiThreadedDLL + MultiThreaded /FS %(AdditionalOptions) $(ProjectDir)../src;%(AdditionalIncludeDirectories) From 04625ea1d12bf9260ca7252cc234ea421d1f6589 Mon Sep 17 00:00:00 2001 From: nekohkr <182421472+nekohkr@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:13:36 +0900 Subject: [PATCH 08/11] Use AdditionalAribSubtitleInfo for subtitle signaling --- Makefile | 82 +- msvc/dantto4k.vcxproj | 10 +- msvc/dantto4k.vcxproj.filters | 26 +- msvc/dantto4k_dll.vcxproj | 22 +- msvc/dantto4k_dll.vcxproj.filters | 38 +- src/additionalAribSubtitleInfo.cpp | 49 + src/additionalAribSubtitleInfo.h | 36 + src/aribCharsetEncoder.cpp | 49 +- src/b24SubtitleConverter.cpp | 78 +- src/b24SubtitleConverter.h | 21 +- src/mmtStream.cpp | 8 +- src/mmtStream.h | 26 +- src/mmtTlvDemuxer.cpp | 48 +- src/mpuProcessorBase.h | 6 +- src/ntp.cpp | 17 +- src/ntp.h | 81 +- src/ntpTimestamp.cpp | 31 + src/ntpTimestamp.h | 17 + src/remuxerHandler.cpp | 181 ++-- src/remuxerHandler.h | 5 +- src/ttml/b24_converter.cpp | 1345 ++++++++++++++-------------- src/ttml/b24_converter.h | 77 +- src/ttml/resolved.h | 110 +-- src/ttml/resolver.cpp | 424 ++++----- src/ttml/resolver.h | 62 +- src/ttml/string_utils.h | 104 +-- src/ttml/sync_mode.h | 28 +- 27 files changed, 1534 insertions(+), 1447 deletions(-) create mode 100644 src/additionalAribSubtitleInfo.cpp create mode 100644 src/additionalAribSubtitleInfo.h create mode 100644 src/ntpTimestamp.cpp create mode 100644 src/ntpTimestamp.h diff --git a/Makefile b/Makefile index 5842f0e..2b84add 100644 --- a/Makefile +++ b/Makefile @@ -1,41 +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) $(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 +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 16eee5e..7a34c2d 100644 --- a/msvc/dantto4k.vcxproj +++ b/msvc/dantto4k.vcxproj @@ -167,6 +167,7 @@ + @@ -191,7 +192,8 @@ - + + @@ -276,6 +278,7 @@ + @@ -306,7 +309,8 @@ - + + @@ -388,4 +392,4 @@ - \ No newline at end of file + diff --git a/msvc/dantto4k.vcxproj.filters b/msvc/dantto4k.vcxproj.filters index 4622590..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 @@ -319,6 +322,9 @@ dantto4k\ttml + + mmttlv\mmt\structs + @@ -573,9 +579,12 @@ mmttlv\tlv\structs - - mmttlv\tlv\structs - + + mmttlv\tlv\structs + + + mmttlv\tlv\structs + mmttlv\mmt\descriptors @@ -708,6 +717,9 @@ dantto4k\ttml + + mmttlv\mmt\structs + @@ -768,4 +780,4 @@ {84aaa3a8-9e4a-4085-9dc5-db67d16195cc} - \ No newline at end of file + diff --git a/msvc/dantto4k_dll.vcxproj b/msvc/dantto4k_dll.vcxproj index 90e0d87..4b14abe 100644 --- a/msvc/dantto4k_dll.vcxproj +++ b/msvc/dantto4k_dll.vcxproj @@ -180,9 +180,10 @@ - - - + + + + @@ -205,7 +206,8 @@ - + + @@ -288,9 +290,10 @@ - - - + + + + @@ -320,7 +323,8 @@ - + + @@ -403,4 +407,4 @@ - \ No newline at end of file + diff --git a/msvc/dantto4k_dll.vcxproj.filters b/msvc/dantto4k_dll.vcxproj.filters index eb5abc5..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,9 +247,12 @@ dantto4k\xml - - dantto4k\subtitle - + + dantto4k\subtitle + + + mmttlv\mmt\structs + dantto4k @@ -573,9 +579,12 @@ mmttlv\tlv\structs - - mmttlv\tlv\structs - + + mmttlv\tlv\structs + + + mmttlv\tlv\structs + mmttlv\mmt\descriptors @@ -603,9 +612,12 @@ dantto4k\xml - - dantto4k\subtitle - + + dantto4k\subtitle + + + mmttlv\mmt\structs + dantto4k\subtitle @@ -768,4 +780,4 @@ {e31c2a9a-285e-49f0-bf5f-da6ccd870531} - \ No newline at end of file + 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 87aa4d2..f4f2a56 100644 --- a/src/aribCharsetEncoder.cpp +++ b/src/aribCharsetEncoder.cpp @@ -73,6 +73,11 @@ const CharLookupTable& charLookupTable() { return table; } +bool isCharsetAllowed(const CharLookupResult& result, EncodeMode encodeMode) { + return encodeMode != EncodeMode::Caption || + result.charset->code != CharsetCode::AdditionalSymbols; +} + std::optional findChar(char32_t c, CharsetCode candidate1, CharsetCode candidate2, EncodeMode encodeMode) { const auto& table = charLookupTable(); auto it = table.find(c); @@ -85,16 +90,12 @@ std::optional findChar(char32_t c, CharsetCode candidate1, Cha return {}; } - if (results.size() == 1) { - return results[0]; - } - if (candidate1 != CharsetCode::None && (encodeMode == EncodeMode::Caption || (candidate1 != CharsetCode::JISKanjiPlane1 && candidate1 != CharsetCode::JISKanjiPlane2))) { for (const auto& res : results) { - if (res.charset->code == candidate1) { + if (isCharsetAllowed(res, encodeMode) && res.charset->code == candidate1) { return res; } } @@ -105,14 +106,18 @@ std::optional findChar(char32_t c, CharsetCode candidate1, Cha (candidate2 != CharsetCode::JISKanjiPlane1 && candidate2 != CharsetCode::JISKanjiPlane2))) { for (const auto& res : results) { - if (res.charset->code == candidate2) { + if (isCharsetAllowed(res, encodeMode) && res.charset->code == candidate2) { return res; } } } - - return results[0]; + const auto result = std::find_if(results.begin(), results.end(), [encodeMode](const auto& value) { + return isCharsetAllowed(value, encodeMode); + }); + return result != results.end() + ? std::optional{*result} + : std::nullopt; } std::optional findCharsetBy2Char(char32_t c1, char32_t c2, CharsetCode candidate1, CharsetCode candidate2, EncodeMode encodeMode) { @@ -129,9 +134,9 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha } const auto& results2 = it2->second; - auto hasCharset = [](const std::vector& list, const Charset* target) { + auto hasCharset = [encodeMode](const std::vector& list, const Charset* target) { for (const auto& res : list) { - if (res.charset == target) { + if (isCharsetAllowed(res, encodeMode) && res.charset == target) { return true; } } @@ -147,7 +152,7 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha bool found1 = false; CharLookupResult res1{}; for (const auto& r : results1) { - if (r.charset == cs) { + if (isCharsetAllowed(r, encodeMode) && r.charset == cs) { found1 = true; res1 = r; break; @@ -169,7 +174,7 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha bool found1 = false; CharLookupResult res1{}; for (const auto& r : results1) { - if (r.charset == cs) { + if (isCharsetAllowed(r, encodeMode) && r.charset == cs) { found1 = true; res1 = r; break; @@ -183,7 +188,7 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha } for (const auto& res1 : results1) { - if (hasCharset(results2, res1.charset)) { + if (isCharsetAllowed(res1, encodeMode) && hasCharset(results2, res1.charset)) { return res1; } } @@ -435,18 +440,13 @@ class Encoder { } } else { - CharsetCode charsetCode = charset->code; - if (mode == EncodeMode::Caption && charsetCode == CharsetCode::JISKanjiPlane1) { - charsetCode = CharsetCode::Kanji; - } - // charset is not in G0-G3 if (charset->code == CharsetCode::JISX0201Katakana) { - setGraphicToCharset(1, charsetCode); + setGraphicToCharset(1, charset->code); setGR(1); } else { - setGraphicToCharset(0, charsetCode); + setGraphicToCharset(0, charset->code); setGL(0); } } @@ -464,17 +464,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); diff --git a/src/b24SubtitleConverter.cpp b/src/b24SubtitleConverter.cpp index fe8cfae..b1d8a21 100644 --- a/src/b24SubtitleConverter.cpp +++ b/src/b24SubtitleConverter.cpp @@ -8,81 +8,7 @@ #include #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()); -} - -void appendCharacterComposition(std::vector& output, uint32_t width, uint32_t height) { - output.push_back(B24ControlSet::CSI); - appendNumber(output, width); - output.push_back(0x3B); - appendNumber(output, height); - output.push_back(B24ControlSet::SP); - output.push_back(B24ControlSet::SSM); -} - -void appendSpacing(std::vector& output, uint32_t spacing, uint8_t control) { - output.push_back(B24ControlSet::CSI); - appendNumber(output, spacing); - output.push_back(B24ControlSet::SP); - output.push_back(control); -} - -void appendTwoDigits(std::vector& output, uint8_t value) { - output.push_back(static_cast(0x30 + value / 10)); - output.push_back(static_cast(0x30 + value % 10)); -} - -void appendOrnament(std::vector& output, uint8_t palette, uint8_t index) { - output.push_back(B24ControlSet::CSI); - output.push_back(0x31); - output.push_back(0x3B); - appendTwoDigits(output, palette); - appendTwoDigits(output, index); - output.push_back(B24ControlSet::SP); - output.push_back(B24ControlSet::ORN); -} - -void appendOrnamentOff(std::vector& output) { - output.push_back(B24ControlSet::CSI); - output.push_back(0x30); - output.push_back(B24ControlSet::SP); - output.push_back(B24ControlSet::ORN); -} - -} - -bool B24SubtitleConverter::convert(const std::string& input, std::list& output, B24::PESData::PESType pesType) { +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; @@ -100,7 +26,7 @@ bool B24SubtitleConverter::convert(const std::string& input, std::listmessage << std::endl; diff --git a/src/b24SubtitleConverter.h b/src/b24SubtitleConverter.h index 3f18ecd..ba8690d 100644 --- a/src/b24SubtitleConverter.h +++ b/src/b24SubtitleConverter.h @@ -6,7 +6,9 @@ #include #include #include -#include +#include +#include +#include "additionalAribSubtitleInfo.h" namespace B24 { enum class DataUnitParameter : uint8_t { @@ -98,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}; @@ -301,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 B24SubtitleConverter { -public: - static bool convert(const std::string& input, std::list& output, B24::PESData::PESType pesType); - +class B24SubtitleConverter { +public: + static bool convert(const std::string& input, std::list& output, B24::PESData::PESType pesType, MmtTlv::SubtitleResolution resolution); + }; 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 f4d941b..fd5f6bd 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 { @@ -492,11 +493,15 @@ void MmtTlvDemuxer::processMmtPackageTable(const Mpt& mpt) { } } - if (!mmtStream) { - continue; - } - - for (const auto& descriptor : asset.descriptors.list) { + if (!mmtStream) { + continue; + } + + if (asset.assetType == AssetType::stpp) { + mmtStream->subtitleInfo.reset(); + } + + for (const auto& descriptor : asset.descriptors.list) { switch (descriptor->getDescriptorTag()) { case MpuTimestampDescriptor::kDescriptorTag: { @@ -532,6 +537,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; + } } } } @@ -729,12 +745,12 @@ void MmtTlvDemuxer::processMpu(Common::ReadStream& stream) { mmtStream->lastMpuSequenceNumber = mpu.mpuSequenceNumber; } - if (mpu.mpuSequenceNumber == *mmtStream->lastMpuSequenceNumber + 1) { - mmtStream->lastMpuSequenceNumber = mpu.mpuSequenceNumber; - mmtStream->auIndex = 0; - } - - auto validator = getFragmentValidator(mmtp.packetId); + if (mpu.mpuSequenceNumber == *mmtStream->lastMpuSequenceNumber + 1) { + mmtStream->lastMpuSequenceNumber = mpu.mpuSequenceNumber; + mmtStream->auIndex = 0; + } + + auto validator = getFragmentValidator(mmtp.packetId); auto assembler = getAssembler(mmtp.packetId); if (mpu.mpuSequenceNumber != mmtStream->lastMpuSequenceNumber) { @@ -803,9 +819,9 @@ 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); - if (ret) { - const auto& mfuData = ret.value(); + auto ret = mmtStream->mpuProcessor->process(*mmtStream, data, mpu.fragmentationIndicator); + if (ret) { + const auto& mfuData = ret.value(); auto* mmtStream = getStreamByIdx(mfuData.streamIndex); if (!mmtStream) { return; @@ -913,4 +929,4 @@ bool MmtTlvDemuxer::isValidTlv(Common::ReadStream& stream) const { return true; } -} \ No newline at end of file +} 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 842bd5f..c672b43 100644 --- a/src/remuxerHandler.cpp +++ b/src/remuxerHandler.cpp @@ -147,48 +147,49 @@ void RemuxerHandler::onAudioData(const MmtTlv::MmtStream& mmtStream, const MmtTl } void RemuxerHandler::onSubtitleData(const MmtTlv::MmtStream& mmtStream, const struct MmtTlv::MfuData& mfuData) { - std::string ttml(mfuData.data.begin(), mfuData.data.end()); - std::list output; - const auto pesType = mmtStream.getComponentTag() == 0x30 - ? B24::PESData::PESType::Synchronized - : B24::PESData::PESType::Asynchronous; - B24SubtitleConverter::convert(ttml, output, pesType); - - if (output.empty()) { + const auto& subtitleInfo = mmtStream.additionalAribSubtitleInfo(); + if (!subtitleInfo) { return; } - if (mmtStream.getComponentTag() != 0x30) { - for (const auto& pesData : output) { - writeSubtitle(mmtStream, pesData, 0); - } + // ARIB-TTML + if (subtitleInfo->subtitleFormat != 0) { return; } - const auto currentPts = lastPcr / 300; - const auto firstTimedOutput = std::find_if(output.begin(), output.end(), - [](const B24SubtitleOutput& subtitle) { - return subtitle.begin.has_value(); - }); - if (!subtitlePtsBase && firstTimedOutput != output.end()) { - subtitlePtsBase = programStartTime * 90000; + const bool synchronized = mmtStream.isClosedCaption(); + if ((!synchronized && !mmtStream.isSuperimposition()) || + (!synchronized && subtitleInfo->tmd != 0b1111)) { + return; + } - if (currentPts != 0) { - const auto firstPts = *subtitlePtsBase + *firstTimedOutput->begin * 90; - const auto minimumPts = currentPts + SUBTITLE_MINIMUM_FUTURE_TIME; - if (firstPts < minimumPts) { - *subtitlePtsBase += minimumPts - firstPts; - } - } + std::string ttml(mfuData.data.begin(), mfuData.data.end()); + std::list 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 (auto& pesData : output) { - const auto pts = pesData.begin - ? subtitlePtsBase.value_or(programStartTime * 90000) + *pesData.begin * 90 - : currentPts == 0 - ? 0 - : currentPts + SUBTITLE_MINIMUM_FUTURE_TIME; - queueSubtitle(mmtStream, std::move(pesData), pts); + if (!pesData.begin) { + continue; + } + const auto pts = referencePts + static_cast(*pesData.begin * 90); + if (pts < 0 || (currentPts != 0 && static_cast(pts) < currentPts)) { + continue; + } + queueSubtitle(mmtStream, std::move(pesData), static_cast(pts)); } if (currentPts != 0) { @@ -354,25 +355,29 @@ void RemuxerHandler::writePendingSubtitles(uint64_t pts) { } } -void RemuxerHandler::writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, uint64_t pts) { - 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) { - if (pts == 0) { - return; - } - pes.setPts(pts); - } - - pes.setStreamId(mmtStream.getComponentTag() == 0x30 - ? STREAM_ID_PRIVATE_STREAM_1 - : STREAM_ID_PRIVATE_STREAM_2); - pes.setPayload(&subtitle.pesData); - pes.setPayloadLength(subtitle.pesData.size()); - if (mmtStream.getComponentTag() == 0x30) { - pes.setPrivateData(&ccis); - pes.setStuffingByteLength(1); + if (synchronized && pts) { + pes.setPts(*pts); + } + + pes.setStreamId(synchronized ? STREAM_ID_PRIVATE_STREAM_1 : STREAM_ID_PRIVATE_STREAM_2); + pes.setPayload(&subtitle.pesData); + pes.setPayloadLength(subtitle.pesData.size()); + if (synchronized) { + pes.setPrivateData(&ccis); + pes.setStuffingByteLength(1); } pes.pack(pesOutput); @@ -425,46 +430,41 @@ 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) { - pes.setPts(lastCaptionManagementDataPts); - } - pes.setStreamId(stream.second.getComponentTag() == 0x30 - ? STREAM_ID_PRIVATE_STREAM_1 - : STREAM_ID_PRIVATE_STREAM_2); + if (synchronized) { + pes.setPts(lastCaptionManagementDataPts); + } + 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) { - pes.setPrivateData(&ccis); + if (synchronized) { + pes.setPrivateData(&ccis); pes.setStuffingByteLength(1); } pes.pack(pesOutput); @@ -622,15 +622,10 @@ 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()) { - std::tm startTime = EITConvertStartTime((mhEit.events.begin())->get()->startTime); - programStartTime = static_cast(std::mktime(&startTime)); - } - - ts::EIT tsEit(true, mhEit.isPf(), 0, mhEit.versionNumber, true, mhEit.serviceId, mhEit.tlvStreamId, mhEit.originalNetworkId); +void RemuxerHandler::onMhEit(const MmtTlv::MhEit& mhEit) { + tsid = mhEit.tlvStreamId; + + 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); @@ -947,12 +942,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); } @@ -1158,17 +1148,10 @@ void RemuxerHandler::onNtp(const MmtTlv::NTPv4& ntp) { lastPcr = ntp.transmit_timestamp.toPcrValue(); - const auto pts = ntp.transmit_timestamp.toPcrValue() / 300; - if (firstPcr && !pendingSubtitles.empty()) { - const auto minimumPts = pts + SUBTITLE_MINIMUM_FUTURE_TIME; - if (pendingSubtitles.front().pts < minimumPts) { - const auto ptsOffset = minimumPts - pendingSubtitles.front().pts; - for (auto& pending : pendingSubtitles) { - pending.pts += ptsOffset; - } - if (subtitlePtsBase) { - *subtitlePtsBase += ptsOffset; - } + const auto pts = static_cast(ntp.transmit_timestamp.toPtsValue()); + if (firstPcr) { + while (!pendingSubtitles.empty() && pendingSubtitles.front().pts < pts) { + pendingSubtitles.pop_front(); } } writeCaptionManagementData(pts); @@ -1183,8 +1166,6 @@ void RemuxerHandler::clear() { mapPesState.clear(); pendingSubtitles.clear(); tsid = -1; - lastPcr = 0; + lastPcr = 0; lastCaptionManagementDataPts = 0; - programStartTime = 0; - subtitlePtsBase.reset(); } diff --git a/src/remuxerHandler.h b/src/remuxerHandler.h index 43ccc63..8abe039 100644 --- a/src/remuxerHandler.h +++ b/src/remuxerHandler.h @@ -93,7 +93,8 @@ class RemuxerHandler : public MmtTlv::DemuxerHandler { void writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTlv::MfuData& mfuData, const std::vector& data); void queueSubtitle(const MmtTlv::MmtStream& mmtStream, B24SubtitleOutput subtitle, uint64_t pts); void writePendingSubtitles(uint64_t pts); - void writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, uint64_t pts); + void writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, + std::optional pts = std::nullopt); void writeCaptionManagementData(uint64_t pts); MmtTlv::MmtTlvDemuxer& demuxer; OutputCallback outputCallback; @@ -106,8 +107,6 @@ class RemuxerHandler : public MmtTlv::DemuxerHandler { int tsid{-1}; uint64_t lastPcr{}; uint64_t lastCaptionManagementDataPts{}; - uint64_t programStartTime{}; - std::optional subtitlePtsBase; 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; diff --git a/src/ttml/b24_converter.cpp b/src/ttml/b24_converter.cpp index a974dd6..ab7ef16 100644 --- a/src/ttml/b24_converter.cpp +++ b/src/ttml/b24_converter.cpp @@ -1,43 +1,57 @@ -#include "ttml/b24_converter.h" -#include "aribTextEncoder.h" -#include "b24Color.h" -#include "b24ControlSet.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace arib { - -namespace ttml { - +#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 kLayoutScale = 0.25; - -struct StyledRun { - std::string_view text; - const StyleProperties* style; -}; +constexpr double kB24LayoutWidth = 960.0; -struct Line { - std::vector runs; -}; +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; + } -struct RegionBlock { - const resolved::Region* region; - std::vector lines; -}; + 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; @@ -50,79 +64,79 @@ enum class SpanRegionMode { 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; + +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) { @@ -164,19 +178,19 @@ class CueCollector { 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; - } - + 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(); @@ -214,232 +228,232 @@ class CueCollector { 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) - : cues_(cues), mode_(mode) { - } - - 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{}; + 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)); - } - + 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) { @@ -449,344 +463,351 @@ class Encoder { 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 * kLayoutScale))); - set_character_spacing(spacing); - } - + + 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 auto spacing = static_cast(std::max(0.0, std::round((line_height - font_height) * kLayoutScale))); + ? 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) { + + if (style.font_size) { const double font_width = style.font_size->x; const double font_height = style.font_size->y; - uint32_t composition_width = 36; - uint32_t composition_height = 36; - uint8_t character_size = B24ControlSet::NSZ; - if (font_width == 72 && font_height == 144) { - character_size = B24ControlSet::MSZ; - } + 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 * kLayoutScale))); - composition_height = static_cast(std::max(1.0, std::round(font_height * kLayoutScale))); - } - set_character_composition(composition_width, composition_height); - set_character_size(character_size); - } - - 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 (!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); + 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.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 (!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 (!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)); - } - } - + 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 * kLayoutScale)); - output_.push_back(0x3B); - append_number(static_cast(extent.y * kLayoutScale)); - output_.push_back(B24ControlSet::SP); - output_.push_back(B24ControlSet::SDF); - } - + 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 * kLayoutScale))); + append_number(static_cast(std::lround(origin.x * layoutScale_))); output_.push_back(0x3B); - append_number(static_cast(std::lround(origin.y * kLayoutScale))); - 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)); - 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: + 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)); + 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_; - 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) { - auto collected = CueCollector{document, mode}.run(); - if (collected.error) { - return { - .outputs = {}, - .error = std::move(collected.error), - }; - } - - return Encoder{collected.cues, mode}.run(); + 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 + +} // namespace ttml + +} // namespace arib diff --git a/src/ttml/b24_converter.h b/src/ttml/b24_converter.h index 0ac15a8..29c325e 100644 --- a/src/ttml/b24_converter.h +++ b/src/ttml/b24_converter.h @@ -1,41 +1,40 @@ -#pragma once - +#pragma once + #include "ttml/resolved.h" #include "ttml/sync_mode.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); - -} // namespace ttml - -} // namespace arib +#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/resolved.h b/src/ttml/resolved.h index e9a5ea6..4ac2ccd 100644 --- a/src/ttml/resolved.h +++ b/src/ttml/resolved.h @@ -1,55 +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 +#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 index 096dd6e..9e1d459 100644 --- a/src/ttml/resolver.cpp +++ b/src/ttml/resolver.cpp @@ -1,185 +1,185 @@ -#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); -} - +#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 } }, + 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 {}; - } + .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()); - + 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, @@ -190,13 +190,13 @@ class Resolver { 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; - } - + + 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); @@ -218,21 +218,21 @@ class Resolver { }; } 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: + 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; @@ -285,22 +285,22 @@ class Resolver { 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_; + } + + 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 + +} // 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 index 6ae4f91..85845e0 100644 --- a/src/ttml/resolver.h +++ b/src/ttml/resolver.h @@ -1,31 +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 +#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 index 486ab08..a6fab5a 100644 --- a/src/ttml/string_utils.h +++ b/src/ttml/string_utils.h @@ -1,52 +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 +#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/sync_mode.h b/src/ttml/sync_mode.h index 4b02fce..70117bf 100644 --- a/src/ttml/sync_mode.h +++ b/src/ttml/sync_mode.h @@ -1,14 +1,14 @@ -#pragma once - -namespace arib { - -namespace ttml { - -enum class SyncMode { - Sync, - Async, -}; - -} // namespace ttml - -} // namespace arib +#pragma once + +namespace arib { + +namespace ttml { + +enum class SyncMode { + Sync, + Async, +}; + +} // namespace ttml + +} // namespace arib From 6e963ac4f3a518359b079be4bfb03e9f3ecccfdc Mon Sep 17 00:00:00 2001 From: nekohkr Date: Thu, 20 Aug 2026 15:13:55 +0900 Subject: [PATCH 09/11] Improve ARIB caption encoding compatibility --- src/aribCharsetEncoder.cpp | 60 ++++++++++++++++++++++---------------- src/remuxerHandler.cpp | 49 ++----------------------------- src/remuxerHandler.h | 12 +------- 3 files changed, 39 insertions(+), 82 deletions(-) diff --git a/src/aribCharsetEncoder.cpp b/src/aribCharsetEncoder.cpp index f4f2a56..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,11 +77,6 @@ const CharLookupTable& charLookupTable() { return table; } -bool isCharsetAllowed(const CharLookupResult& result, EncodeMode encodeMode) { - return encodeMode != EncodeMode::Caption || - result.charset->code != CharsetCode::AdditionalSymbols; -} - std::optional findChar(char32_t c, CharsetCode candidate1, CharsetCode candidate2, EncodeMode encodeMode) { const auto& table = charLookupTable(); auto it = table.find(c); @@ -90,12 +89,21 @@ std::optional findChar(char32_t c, CharsetCode candidate1, Cha return {}; } + 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 && (encodeMode == EncodeMode::Caption || (candidate1 != CharsetCode::JISKanjiPlane1 && candidate1 != CharsetCode::JISKanjiPlane2))) { for (const auto& res : results) { - if (isCharsetAllowed(res, encodeMode) && res.charset->code == candidate1) { + if (res.charset->code == candidate1) { return res; } } @@ -106,18 +114,13 @@ std::optional findChar(char32_t c, CharsetCode candidate1, Cha (candidate2 != CharsetCode::JISKanjiPlane1 && candidate2 != CharsetCode::JISKanjiPlane2))) { for (const auto& res : results) { - if (isCharsetAllowed(res, encodeMode) && res.charset->code == candidate2) { + if (res.charset->code == candidate2) { return res; } } } - const auto result = std::find_if(results.begin(), results.end(), [encodeMode](const auto& value) { - return isCharsetAllowed(value, encodeMode); - }); - return result != results.end() - ? std::optional{*result} - : std::nullopt; + return results.front(); } std::optional findCharsetBy2Char(char32_t c1, char32_t c2, CharsetCode candidate1, CharsetCode candidate2, EncodeMode encodeMode) { @@ -134,9 +137,9 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha } const auto& results2 = it2->second; - auto hasCharset = [encodeMode](const std::vector& list, const Charset* target) { + auto hasCharset = [](const std::vector& list, const Charset* target) { for (const auto& res : list) { - if (isCharsetAllowed(res, encodeMode) && res.charset == target) { + if (res.charset == target) { return true; } } @@ -152,7 +155,7 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha bool found1 = false; CharLookupResult res1{}; for (const auto& r : results1) { - if (isCharsetAllowed(r, encodeMode) && r.charset == cs) { + if (r.charset == cs) { found1 = true; res1 = r; break; @@ -174,7 +177,7 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha bool found1 = false; CharLookupResult res1{}; for (const auto& r : results1) { - if (isCharsetAllowed(r, encodeMode) && r.charset == cs) { + if (r.charset == cs) { found1 = true; res1 = r; break; @@ -188,7 +191,7 @@ std::optional findCharsetBy2Char(char32_t c1, char32_t c2, Cha } for (const auto& res1 : results1) { - if (isCharsetAllowed(res1, encodeMode) && hasCharset(results2, res1.charset)) { + if (hasCharset(results2, res1.charset)) { return res1; } } @@ -365,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) { @@ -608,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); @@ -623,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/remuxerHandler.cpp b/src/remuxerHandler.cpp index c672b43..c1ca040 100644 --- a/src/remuxerHandler.cpp +++ b/src/remuxerHandler.cpp @@ -44,9 +44,6 @@ namespace { -constexpr uint64_t SUBTITLE_PRELOAD_TIME = 90 * 1000; -constexpr uint64_t SUBTITLE_MINIMUM_FUTURE_TIME = 90 * 200; - int convertRunningStatus(int runningStatus) { switch (runningStatus) { case 0: @@ -181,7 +178,7 @@ void RemuxerHandler::onSubtitleData(const MmtTlv::MmtStream& mmtStream, const st } const auto referencePts = subtitleInfo->referenceStartTime.toPtsValue(); - for (auto& pesData : output) { + for (const auto& pesData : output) { if (!pesData.begin) { continue; } @@ -189,11 +186,7 @@ void RemuxerHandler::onSubtitleData(const MmtTlv::MmtStream& mmtStream, const st if (pts < 0 || (currentPts != 0 && static_cast(pts) < currentPts)) { continue; } - queueSubtitle(mmtStream, std::move(pesData), static_cast(pts)); - } - - if (currentPts != 0) { - writePendingSubtitles(currentPts); + writeSubtitle(mmtStream, pesData, static_cast(pts)); } } @@ -327,34 +320,6 @@ void RemuxerHandler::writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTl } } -void RemuxerHandler::queueSubtitle(const MmtTlv::MmtStream& mmtStream, B24SubtitleOutput subtitle, uint64_t pts) { - const auto position = std::upper_bound( - pendingSubtitles.begin(), pendingSubtitles.end(), pts, - [](uint64_t lhs, const PendingSubtitle& rhs) { - return lhs < rhs.pts; - }); - pendingSubtitles.insert(position, PendingSubtitle{ - .packetId = mmtStream.getPacketId(), - .pts = pts, - .subtitle = std::move(subtitle), - }); -} - -void RemuxerHandler::writePendingSubtitles(uint64_t pts) { - const auto preloadPts = pts + SUBTITLE_PRELOAD_TIME; - while (!pendingSubtitles.empty() && pendingSubtitles.front().pts <= preloadPts) { - auto pending = std::move(pendingSubtitles.front()); - pendingSubtitles.pop_front(); - - const auto* stream = demuxer.getStream(pending.packetId); - if (!stream) { - continue; - } - - writeSubtitle(*stream, pending.subtitle, pending.pts); - } -} - void RemuxerHandler::writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, std::optional pts) { const auto& subtitleInfo = mmtStream.additionalAribSubtitleInfo(); if (!subtitleInfo) { @@ -1134,7 +1099,6 @@ void RemuxerHandler::onNit(const MmtTlv::Nit& nit) { } void RemuxerHandler::onNtp(const MmtTlv::NTPv4& ntp) { - const bool firstPcr = lastPcr == 0; auto& cc = mapCC[PCR_PID]; ts::TSPacket packet; packet.init(PCR_PID, cc & 0xF, 0); @@ -1149,13 +1113,7 @@ void RemuxerHandler::onNtp(const MmtTlv::NTPv4& ntp) { lastPcr = ntp.transmit_timestamp.toPcrValue(); const auto pts = static_cast(ntp.transmit_timestamp.toPtsValue()); - if (firstPcr) { - while (!pendingSubtitles.empty() && pendingSubtitles.front().pts < pts) { - pendingSubtitles.pop_front(); - } - } writeCaptionManagementData(pts); - writePendingSubtitles(pts); } void RemuxerHandler::clear() { @@ -1164,8 +1122,7 @@ void RemuxerHandler::clear() { mapPesPendingData.clear(); mapPesPacketIndex.clear(); mapPesState.clear(); - pendingSubtitles.clear(); - tsid = -1; + tsid = -1; lastPcr = 0; lastCaptionManagementDataPts = 0; } diff --git a/src/remuxerHandler.h b/src/remuxerHandler.h index 8abe039..0e84746 100644 --- a/src/remuxerHandler.h +++ b/src/remuxerHandler.h @@ -3,7 +3,6 @@ #include "b24SubtitleConverter.h" #include "damt.h" #include -#include #include #include #include @@ -84,15 +83,7 @@ class RemuxerHandler : public MmtTlv::DemuxerHandler { Init, InFragment, }; - struct PendingSubtitle { - uint16_t packetId; - uint64_t pts; - B24SubtitleOutput subtitle; - }; - void writeStream(const MmtTlv::MmtStream& mmtStream, const MmtTlv::MfuData& mfuData, const std::vector& data); - void queueSubtitle(const MmtTlv::MmtStream& mmtStream, B24SubtitleOutput subtitle, uint64_t pts); - void writePendingSubtitles(uint64_t pts); void writeSubtitle(const MmtTlv::MmtStream& mmtStream, const B24SubtitleOutput& subtitle, std::optional pts = std::nullopt); void writeCaptionManagementData(uint64_t pts); @@ -103,8 +94,7 @@ class RemuxerHandler : public MmtTlv::DemuxerHandler { std::unordered_map> mapPesPendingData; std::unordered_map mapPesPacketIndex; std::unordered_map mapPesState; - std::list pendingSubtitles; - int tsid{-1}; + 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, }; From ae474e8dd669cc209fe5afbbae1a073c1f61d0b3 Mon Sep 17 00:00:00 2001 From: nekohkr Date: Thu, 20 Aug 2026 20:02:31 +0900 Subject: [PATCH 10/11] Improve ARIB caption encoding compatibility --- src/ttml/b24_converter.cpp | 45 +++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/ttml/b24_converter.cpp b/src/ttml/b24_converter.cpp index ab7ef16..5ef7d36 100644 --- a/src/ttml/b24_converter.cpp +++ b/src/ttml/b24_converter.cpp @@ -506,6 +506,18 @@ class Encoder { 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); } @@ -525,19 +537,7 @@ class Encoder { font |= 0x02; } set_font(font); - - 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 (!aps) { append_aps(0, 0); @@ -684,13 +684,18 @@ class Encoder { 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(0x40 | index)); - - last_text_color_palette_ = palette; + 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; } From 0c5e4a82d6577602a05c2d6338c5403ea06ef0b1 Mon Sep 17 00:00:00 2001 From: nekohkr Date: Fri, 21 Aug 2026 20:21:23 +0900 Subject: [PATCH 11/11] Clamp subtitle PTS --- src/remuxerHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/remuxerHandler.cpp b/src/remuxerHandler.cpp index 8605fd6..4070e91 100644 --- a/src/remuxerHandler.cpp +++ b/src/remuxerHandler.cpp @@ -183,10 +183,10 @@ void RemuxerHandler::onSubtitleData(const MmtTlv::MmtStream& mmtStream, const st continue; } const auto pts = referencePts + static_cast(*pesData.begin * 90); - if (pts < 0 || (currentPts != 0 && static_cast(pts) < currentPts)) { + if (pts < 0) { continue; } - writeSubtitle(mmtStream, pesData, static_cast(pts)); + writeSubtitle(mmtStream, pesData, std::max(static_cast(pts), currentPts)); } }