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]);
+}