diff --git a/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj b/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj index e66255ba5..9db8cbaff 100644 --- a/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj +++ b/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj @@ -461,6 +461,10 @@ RelativePath="..\..\libaegisub\include\libaegisub\line_iterator.h" > + + diff --git a/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj b/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj index 13937cfe9..9f7337eb2 100644 --- a/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj +++ b/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj @@ -59,6 +59,7 @@ + diff --git a/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj.filters b/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj.filters index d2d4de890..0bcd5028e 100644 --- a/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj.filters +++ b/aegisub/build/msbuild/libaegisub/libaegisub.vcxproj.filters @@ -56,6 +56,9 @@ Header Files + + Header Files + Header Files diff --git a/aegisub/build/tests_vs2008/tests_vs2008.vcproj b/aegisub/build/tests_vs2008/tests_vs2008.vcproj index 65de85689..e2d22db0a 100644 --- a/aegisub/build/tests_vs2008/tests_vs2008.vcproj +++ b/aegisub/build/tests_vs2008/tests_vs2008.vcproj @@ -322,6 +322,10 @@ RelativePath="..\..\tests\libaegisub_line_iterator.cpp" > + + diff --git a/aegisub/libaegisub/include/libaegisub/line_wrap.h b/aegisub/libaegisub/include/libaegisub/line_wrap.h new file mode 100644 index 000000000..76ed31e8b --- /dev/null +++ b/aegisub/libaegisub/include/libaegisub/line_wrap.h @@ -0,0 +1,173 @@ +// Copyright (c) 2012, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// $Id$ + +/// @file line_wrap.h +/// @brief Generic paragraph formatting logic + +#ifndef LAGI_PRE +#include +#include +#include +#include +#endif + +namespace agi { + enum WrapMode { + /// Semi-balanced, with the first line guaranteed to be longest if possible + Wrap_Balanced_FirstLonger = 0, + /// Simple greedy matching with breaks as late as possible + Wrap_Greedy = 1, + /// No line breaking at all + Wrap_None = 2, + /// Semi-balanced, with the last line guaranteed to be longest if possible + Wrap_Balanced_LastLonger = 3, + /// Balanced, with lines as close to equal in length as possible + Wrap_Balanced = 4 + }; + + namespace line_wrap_detail { + template + Width waste(Width width, Width desired) { + return (width - desired) * (width - desired); + } + + template + inline void get_line_widths(StartCont const& line_start_points, Iter begin, Iter end, WidthCont &line_widths) { + size_t line_start = 0; + for (size_t i = 0; i < line_start_points.size(); ++i) { + line_widths.push_back(std::accumulate(begin + line_start, begin + line_start_points[i], 0)); + line_start = line_start_points[i]; + } + line_widths.push_back(std::accumulate(begin + line_start, end, 0)); + } + + // For first-longer and last-longer, bubble words forward/backwards when + // possible and needed to make the first/last lines longer + // + // This is done rather than just using VSFilter's simpler greedy + // algorithm due to that VSFilter's algorithm is incorrect; it can + // produce incorrectly unbalanced lines and even excess line breaks + template + void unbalance(StartCont &ret, WidthCont const& widths, Width max_width, WrapMode wrap_mode) { + WidthCont line_widths; + get_line_widths(ret, widths.begin(), widths.end(), line_widths); + + int from_offset = 0; + int to_offset = 1; + if (wrap_mode == agi::Wrap_Balanced_LastLonger) + std::swap(from_offset, to_offset); + + for (size_t i = 0; i < ret.size(); ++i) { + // shift words until they're unbalanced in the correct direction + // or shifting a word would exceed the length limit + while (line_widths[i + from_offset] < line_widths[i + to_offset]) { + int shift_word_width = widths[ret[i]]; + if (line_widths[i + from_offset] + shift_word_width > max_width) + break; + + line_widths[i + from_offset] += shift_word_width; + line_widths[i + to_offset] -= shift_word_width; + ret[i] += to_offset + -from_offset; + } + } + } + + template + void break_greedy(StartCont &ret, WidthCont const& widths, Width max_width) { + // Simple greedy matching that just starts a new line every time the + // max length is exceeded + Width cur_line_width = 0; + for (size_t i = 0; i < widths.size(); ++i) { + if (cur_line_width > 0 && widths[i] + cur_line_width > max_width) { + ret.push_back(i); + cur_line_width = 0; + } + + cur_line_width += widths[i]; + } + } + } + + /// Get the indices at which the blocks should be wrapped + /// @tparam WidthCont A random-access container of Widths + /// @tparam Width A numeric type which represents a width + /// @param widths The widths of the objects to fit within the space + /// @param max_width The available space for the objects + /// @param wrap_mode WrapMode to use to decide where to insert breaks + /// @return Indices into widths which breaks should be inserted before + template + std::vector get_wrap_points(WidthCont const& widths, Width max_width, WrapMode wrap_mode) { + using namespace line_wrap_detail; + + std::vector ret; + + if (wrap_mode == Wrap_None || widths.size() < 2) + return ret; + + // Check if any wrapping is actually needed + Width total_width = std::accumulate(widths.begin(), widths.end(), 0); + if (total_width <= max_width) + return ret; + + + if (wrap_mode == Wrap_Greedy) { + break_greedy(ret, widths, max_width); + return ret; + } + + size_t num_words = distance(widths.begin(), widths.end()); + + // the cost of the optimal arrangement of words [0..i] + std::vector optimal_costs(num_words, INT_MAX); + + // the optimal start word for a line ending at i + std::vector line_starts(num_words, INT_MAX); + + // O(num_words * min(num_words, max_width)) + for (size_t end_word = 0; end_word < num_words; ++end_word) { + Width current_line_width = 0; + for (int start_word = end_word; start_word >= 0; --start_word) { + current_line_width += widths[start_word]; + + // Only evaluate lines over the limit if they're one word + if (current_line_width > max_width && (size_t)start_word != end_word) + break; + + Width cost = waste(current_line_width, max_width); + + if (start_word > 0) + cost += optimal_costs[start_word - 1]; + + if (cost < optimal_costs[end_word]) { + optimal_costs[end_word] = cost; + line_starts[end_word] = start_word; + } + } + } + + // Select the optimal start word for each line ending with last_word + for (size_t last_word = num_words; last_word > 0 && line_starts[last_word - 1] > 0; last_word = line_starts[last_word]) { + --last_word; + ret.push_back(line_starts[last_word]); + } + std::reverse(ret.begin(), ret.end()); + + if (wrap_mode != Wrap_Balanced) + unbalance(ret, widths, max_width, wrap_mode); + + return ret; + } +} diff --git a/aegisub/tests/Makefile b/aegisub/tests/Makefile index 13c887bea..f6baef9c0 100644 --- a/aegisub/tests/Makefile +++ b/aegisub/tests/Makefile @@ -28,7 +28,8 @@ SRC = \ libaegisub_signals.cpp \ libaegisub_thesaurus.cpp \ libaegisub_util.cpp \ - libaegisub_vfr.cpp + libaegisub_vfr.cpp \ + libaegisub_line_wrap.cpp HEADER = \ *.h diff --git a/aegisub/tests/libaegisub_line_wrap.cpp b/aegisub/tests/libaegisub_line_wrap.cpp new file mode 100644 index 000000000..cd522fb33 --- /dev/null +++ b/aegisub/tests/libaegisub_line_wrap.cpp @@ -0,0 +1,158 @@ +// Copyright (c) 2012, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// $Id$ + +/// @file libaegisub_line_wrap.cpp +/// @brief agi::get_wrap_points tests + +#include + +#include "main.h" +#include "util.h" + +using namespace agi; +using namespace util; + +TEST(lagi_wrap, no_wrapping_needed) { + for (int i = Wrap_Balanced_FirstLonger; i <= Wrap_Balanced_LastLonger; ++i) + ASSERT_NO_THROW(get_wrap_points(make_vector(0), 100, (agi::WrapMode)i)); + + for (int i = Wrap_Balanced_FirstLonger; i <= Wrap_Balanced_LastLonger; ++i) + ASSERT_NO_THROW(get_wrap_points(make_vector(1, 10), 100, (agi::WrapMode)i)); + + for (int i = Wrap_Balanced_FirstLonger; i <= Wrap_Balanced_LastLonger; ++i) + EXPECT_TRUE(get_wrap_points(make_vector(1, 99), 100, (agi::WrapMode)i).empty()); + + for (int i = Wrap_Balanced_FirstLonger; i <= Wrap_Balanced_LastLonger; ++i) + EXPECT_TRUE(get_wrap_points(make_vector(4, 25, 25, 25, 24), 100, (agi::WrapMode)i).empty()); + + for (int i = Wrap_Balanced_FirstLonger; i <= Wrap_Balanced_LastLonger; ++i) + EXPECT_TRUE(get_wrap_points(make_vector(1, 101), 100, (agi::WrapMode)i).empty()); +} + +TEST(lagi_wrap, greedy) { + std::vector ret; + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 20, Wrap_Greedy)); + EXPECT_EQ(0, ret.size()); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 19, Wrap_Greedy)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 9, Wrap_Greedy)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(4, 5, 5, 5, 1), 15, Wrap_Greedy)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(3, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(std::vector(10, 3), 7, Wrap_Greedy)); + ASSERT_EQ(4, ret.size()); + EXPECT_EQ(2, ret[0]); + EXPECT_EQ(4, ret[1]); + EXPECT_EQ(6, ret[2]); + EXPECT_EQ(8, ret[3]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(9, 6, 7, 6, 8, 10, 10, 3, 4, 10), 20, Wrap_Greedy)); + ASSERT_EQ(3, ret.size()); + EXPECT_EQ(3, ret[0]); + EXPECT_EQ(5, ret[1]); + EXPECT_EQ(8, ret[2]); +} + +TEST(lagi_wrap, first_longer) { + std::vector ret; + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 20, Wrap_Balanced_FirstLonger)); + EXPECT_EQ(0, ret.size()); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 19, Wrap_Balanced_FirstLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 9, Wrap_Balanced_FirstLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(3, 5, 5, 1), 10, Wrap_Balanced_FirstLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(2, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(4, 5, 5, 5, 1), 15, Wrap_Balanced_FirstLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(2, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(3, 6, 5, 5), 10, Wrap_Balanced_FirstLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); +} + +TEST(lagi_wrap, last_longer) { + std::vector ret; + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 20, Wrap_Balanced_LastLonger)); + EXPECT_EQ(0, ret.size()); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 19, Wrap_Balanced_LastLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 9, Wrap_Balanced_LastLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(3, 5, 5, 1), 10, Wrap_Balanced_LastLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(4, 5, 5, 5, 1), 15, Wrap_Balanced_LastLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(3, 5, 5, 6), 10, Wrap_Balanced_LastLonger)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(2, ret[0]); +} + +TEST(lagi_wrap, balanced) { + std::vector ret; + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 20, Wrap_Balanced)); + EXPECT_EQ(0, ret.size()); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 19, Wrap_Balanced)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(2, 10, 10), 9, Wrap_Balanced)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(3, 5, 5, 1), 10, Wrap_Balanced)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(1, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(4, 5, 5, 5, 1), 15, Wrap_Balanced)); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ(2, ret[0]); + + ASSERT_NO_THROW(ret = get_wrap_points(make_vector(9, 6, 7, 6, 8, 10, 10, 3, 4, 10), 20, Wrap_Balanced)); + ASSERT_EQ(3, ret.size()); + EXPECT_EQ(3, ret[0]); + EXPECT_EQ(5, ret[1]); + EXPECT_EQ(7, ret[2]); +}