Add line-wrapping logic to libaegisub

Originally committed to SVN as r6635.
This commit is contained in:
Thomas Goyne 2012-03-29 19:05:16 +00:00
parent 39ca0c1b5b
commit 028fd3b4ba
7 changed files with 345 additions and 1 deletions

View file

@ -461,6 +461,10 @@
RelativePath="..\..\libaegisub\include\libaegisub\line_iterator.h" RelativePath="..\..\libaegisub\include\libaegisub\line_iterator.h"
> >
</File> </File>
<File
RelativePath="..\..\libaegisub\include\libaegisub\line_wrap.h"
>
</File>
<File <File
RelativePath="..\..\libaegisub\include\libaegisub\log.h" RelativePath="..\..\libaegisub\include\libaegisub\log.h"
> >

View file

@ -59,6 +59,7 @@
<ClInclude Include="$(SrcDir)include\libaegisub\json.h" /> <ClInclude Include="$(SrcDir)include\libaegisub\json.h" />
<ClInclude Include="$(SrcDir)include\libaegisub\keyframe.h" /> <ClInclude Include="$(SrcDir)include\libaegisub\keyframe.h" />
<ClInclude Include="$(SrcDir)include\libaegisub\line_iterator.h" /> <ClInclude Include="$(SrcDir)include\libaegisub\line_iterator.h" />
<ClInclude Include="$(SrcDir)include\libaegisub\line_wrap.h" />
<ClInclude Include="$(SrcDir)include\libaegisub\log.h" /> <ClInclude Include="$(SrcDir)include\libaegisub\log.h" />
<ClInclude Include="$(SrcDir)include\libaegisub\mru.h" /> <ClInclude Include="$(SrcDir)include\libaegisub\mru.h" />
<ClInclude Include="$(SrcDir)include\libaegisub\option.h" /> <ClInclude Include="$(SrcDir)include\libaegisub\option.h" />

View file

@ -56,6 +56,9 @@
<ClInclude Include="$(SrcDir)include\libaegisub\line_iterator.h"> <ClInclude Include="$(SrcDir)include\libaegisub\line_iterator.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="$(SrcDir)include\libaegisub\line_wrap.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="$(SrcDir)include\libaegisub\log.h"> <ClInclude Include="$(SrcDir)include\libaegisub\log.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>

View file

@ -322,6 +322,10 @@
RelativePath="..\..\tests\libaegisub_line_iterator.cpp" RelativePath="..\..\tests\libaegisub_line_iterator.cpp"
> >
</File> </File>
<File
RelativePath="..\..\tests\libaegisub_line_wrap.cpp"
>
</File>
<File <File
RelativePath="..\..\tests\libaegisub_mru.cpp" RelativePath="..\..\tests\libaegisub_mru.cpp"
> >

View file

@ -0,0 +1,173 @@
// Copyright (c) 2012, Thomas Goyne <plorkyeran@aegisub.org>
//
// 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 <algorithm>
#include <climits>
#include <numeric>
#include <vector>
#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<class Width>
Width waste(Width width, Width desired) {
return (width - desired) * (width - desired);
}
template<class StartCont, class Iter, class WidthCont>
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<class StartCont, class WidthCont, class Width>
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<class StartCont, class WidthCont, class Width>
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<class WidthCont, class Width>
std::vector<size_t> get_wrap_points(WidthCont const& widths, Width max_width, WrapMode wrap_mode) {
using namespace line_wrap_detail;
std::vector<size_t> 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<Width> optimal_costs(num_words, INT_MAX);
// the optimal start word for a line ending at i
std::vector<size_t> 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;
}
}

View file

@ -28,7 +28,8 @@ SRC = \
libaegisub_signals.cpp \ libaegisub_signals.cpp \
libaegisub_thesaurus.cpp \ libaegisub_thesaurus.cpp \
libaegisub_util.cpp \ libaegisub_util.cpp \
libaegisub_vfr.cpp libaegisub_vfr.cpp \
libaegisub_line_wrap.cpp
HEADER = \ HEADER = \
*.h *.h

View file

@ -0,0 +1,158 @@
// Copyright (c) 2012, Thomas Goyne <plorkyeran@aegisub.org>
//
// 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 <libaegisub/line_wrap.h>
#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<int>(0), 100, (agi::WrapMode)i));
for (int i = Wrap_Balanced_FirstLonger; i <= Wrap_Balanced_LastLonger; ++i)
ASSERT_NO_THROW(get_wrap_points(make_vector<int>(1, 10), 100, (agi::WrapMode)i));
for (int i = Wrap_Balanced_FirstLonger; i <= Wrap_Balanced_LastLonger; ++i)
EXPECT_TRUE(get_wrap_points(make_vector<int>(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<int>(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<int>(1, 101), 100, (agi::WrapMode)i).empty());
}
TEST(lagi_wrap, greedy) {
std::vector<size_t> ret;
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(2, 10, 10), 20, Wrap_Greedy));
EXPECT_EQ(0, ret.size());
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(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<int>(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<int>(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<int>(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<int>(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<size_t> ret;
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(2, 10, 10), 20, Wrap_Balanced_FirstLonger));
EXPECT_EQ(0, ret.size());
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(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<int>(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<int>(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<int>(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<int>(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<size_t> ret;
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(2, 10, 10), 20, Wrap_Balanced_LastLonger));
EXPECT_EQ(0, ret.size());
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(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<int>(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<int>(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<int>(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<int>(3, 5, 5, 6), 10, Wrap_Balanced_LastLonger));
ASSERT_EQ(1, ret.size());
EXPECT_EQ(2, ret[0]);
}
TEST(lagi_wrap, balanced) {
std::vector<size_t> ret;
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(2, 10, 10), 20, Wrap_Balanced));
EXPECT_EQ(0, ret.size());
ASSERT_NO_THROW(ret = get_wrap_points(make_vector<int>(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<int>(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<int>(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<int>(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<int>(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]);
}