forked from mia/Aegisub
169 lines
6 KiB
C++
169 lines
6 KiB
C++
// 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.
|
|
|
|
/// @file line_wrap.h
|
|
/// @brief Generic paragraph formatting logic
|
|
|
|
#include <algorithm>
|
|
#include <climits>
|
|
#include <numeric>
|
|
#include <vector>
|
|
|
|
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 (auto & line_start_point : line_start_points) {
|
|
line_widths.push_back(std::accumulate(begin + line_start, begin + line_start_point, 0));
|
|
line_start = line_start_point;
|
|
}
|
|
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;
|
|
}
|
|
}
|