Add line-wrapping logic to libaegisub
Originally committed to SVN as r6635.
This commit is contained in:
parent
39ca0c1b5b
commit
028fd3b4ba
7 changed files with 345 additions and 1 deletions
|
@ -461,6 +461,10 @@
|
|||
RelativePath="..\..\libaegisub\include\libaegisub\line_iterator.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\libaegisub\include\libaegisub\line_wrap.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\libaegisub\include\libaegisub\log.h"
|
||||
>
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
<ClInclude Include="$(SrcDir)include\libaegisub\json.h" />
|
||||
<ClInclude Include="$(SrcDir)include\libaegisub\keyframe.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\mru.h" />
|
||||
<ClInclude Include="$(SrcDir)include\libaegisub\option.h" />
|
||||
|
|
|
@ -56,6 +56,9 @@
|
|||
<ClInclude Include="$(SrcDir)include\libaegisub\line_iterator.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="$(SrcDir)include\libaegisub\line_wrap.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="$(SrcDir)include\libaegisub\log.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
|
|
|
@ -322,6 +322,10 @@
|
|||
RelativePath="..\..\tests\libaegisub_line_iterator.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\tests\libaegisub_line_wrap.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\tests\libaegisub_mru.cpp"
|
||||
>
|
||||
|
|
173
aegisub/libaegisub/include/libaegisub/line_wrap.h
Normal file
173
aegisub/libaegisub/include/libaegisub/line_wrap.h
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
158
aegisub/tests/libaegisub_line_wrap.cpp
Normal file
158
aegisub/tests/libaegisub_line_wrap.cpp
Normal 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]);
|
||||
}
|
Loading…
Reference in a new issue