2012-09-04 15:33:15 +00:00
|
|
|
/*
|
2012-12-11 12:48:01 +00:00
|
|
|
* TextLineBuffer.cc
|
2012-09-04 15:33:15 +00:00
|
|
|
*
|
|
|
|
* Generate and optimized HTML for one line
|
|
|
|
*
|
2013-02-05 06:36:36 +00:00
|
|
|
* Copyright (C) 2012,2013 Lu Wang <coolwanglu@gmail.com>
|
2012-09-04 15:33:15 +00:00
|
|
|
*/
|
|
|
|
|
2012-09-05 07:13:21 +00:00
|
|
|
#include <vector>
|
2013-03-21 04:18:26 +00:00
|
|
|
#include <cmath>
|
2013-03-20 15:46:58 +00:00
|
|
|
#include <algorithm>
|
2012-09-05 07:13:21 +00:00
|
|
|
|
2012-09-04 15:33:15 +00:00
|
|
|
#include "HTMLRenderer.h"
|
2012-12-11 12:48:01 +00:00
|
|
|
#include "TextLineBuffer.h"
|
2012-11-29 09:28:05 +00:00
|
|
|
#include "util/namespace.h"
|
2012-11-29 09:45:26 +00:00
|
|
|
#include "util/unicode.h"
|
2012-11-29 10:28:07 +00:00
|
|
|
#include "util/math.h"
|
2013-02-27 18:11:34 +00:00
|
|
|
#include "util/css_const.h"
|
2013-02-15 05:07:00 +00:00
|
|
|
#include "util/encoding.h"
|
2012-09-04 15:33:15 +00:00
|
|
|
|
2012-09-12 15:26:14 +00:00
|
|
|
namespace pdf2htmlEX {
|
|
|
|
|
2012-09-04 15:33:15 +00:00
|
|
|
using std::min;
|
|
|
|
using std::max;
|
2012-09-05 07:13:21 +00:00
|
|
|
using std::vector;
|
2012-09-10 17:53:33 +00:00
|
|
|
using std::ostream;
|
2012-11-29 10:28:07 +00:00
|
|
|
using std::cerr;
|
|
|
|
using std::endl;
|
2013-03-20 15:46:58 +00:00
|
|
|
using std::find;
|
2013-03-21 04:18:26 +00:00
|
|
|
using std::abs;
|
2012-09-04 15:33:15 +00:00
|
|
|
|
2012-12-11 12:48:01 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::append_unicodes(const Unicode * u, int l)
|
2012-09-04 15:33:15 +00:00
|
|
|
{
|
|
|
|
text.insert(text.end(), u, u+l);
|
|
|
|
}
|
|
|
|
|
2012-12-11 12:48:01 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::append_offset(double width)
|
2012-09-04 15:33:15 +00:00
|
|
|
{
|
2013-04-03 17:35:44 +00:00
|
|
|
/*
|
|
|
|
* If the last offset is very thin, we can ignore it and directly use it
|
|
|
|
* But this should not happen often, and we will also filter near-zero offsets when outputting them
|
|
|
|
* So don't check it
|
|
|
|
*/
|
2012-09-04 15:33:15 +00:00
|
|
|
if((!offsets.empty()) && (offsets.back().start_idx == text.size()))
|
|
|
|
offsets.back().width += width;
|
|
|
|
else
|
2013-04-03 17:35:44 +00:00
|
|
|
offsets.emplace_back(text.size(), width);
|
2012-09-04 15:33:15 +00:00
|
|
|
}
|
|
|
|
|
2013-04-04 13:19:28 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::append_state(const HTMLState & html_state)
|
2012-09-04 15:33:15 +00:00
|
|
|
{
|
2013-04-04 13:19:28 +00:00
|
|
|
if(states.empty())
|
|
|
|
{
|
|
|
|
// if this is the first state, update position and matrix
|
|
|
|
x = html_state.x;
|
|
|
|
y = html_state.y;
|
|
|
|
tm_id = renderer->transform_matrix_manager.install(html_state.transform_matrix);
|
|
|
|
}
|
2012-09-04 15:33:15 +00:00
|
|
|
if(states.empty() || (states.back().start_idx != text.size()))
|
|
|
|
{
|
2013-04-03 17:35:44 +00:00
|
|
|
states.emplace_back();
|
2012-09-04 15:33:15 +00:00
|
|
|
states.back().start_idx = text.size();
|
2013-03-20 15:46:58 +00:00
|
|
|
states.back().hash_umask = 0;
|
2012-09-04 15:33:15 +00:00
|
|
|
}
|
|
|
|
|
2013-04-04 13:19:28 +00:00
|
|
|
states.back().html_state = html_state;
|
2012-09-04 15:33:15 +00:00
|
|
|
}
|
|
|
|
|
2012-12-11 12:48:01 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::flush(void)
|
2012-09-04 15:33:15 +00:00
|
|
|
{
|
|
|
|
/*
|
2012-12-11 12:52:36 +00:00
|
|
|
* Each Line is an independent absolute positioned block
|
2012-09-04 15:33:15 +00:00
|
|
|
* so even we have a few states or offsets, we may omit them
|
|
|
|
*/
|
2013-04-03 08:28:06 +00:00
|
|
|
if(text.empty())
|
|
|
|
{
|
|
|
|
states.clear();
|
|
|
|
offsets.clear();
|
|
|
|
return;
|
|
|
|
}
|
2012-09-04 15:33:15 +00:00
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// remove unuseful states in the end
|
2013-04-03 06:04:39 +00:00
|
|
|
while((!states.empty()) && (states.back().start_idx >= text.size()))
|
|
|
|
states.pop_back();
|
|
|
|
|
2012-09-04 15:33:15 +00:00
|
|
|
if(states.empty() || (states[0].start_idx != 0))
|
|
|
|
{
|
|
|
|
cerr << "Warning: text without a style! Must be a bug in pdf2htmlEX" << endl;
|
2013-04-03 08:28:06 +00:00
|
|
|
text.clear();
|
|
|
|
offsets.clear();
|
2012-09-04 15:33:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// optimize before output
|
2013-03-20 15:46:58 +00:00
|
|
|
optimize();
|
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// Start Output
|
2013-01-28 10:46:44 +00:00
|
|
|
ostream & out = renderer->f_pages.fs;
|
2013-04-03 06:04:39 +00:00
|
|
|
{
|
2013-04-03 17:35:44 +00:00
|
|
|
// max_ascent determines the height of the div
|
|
|
|
double max_ascent = 0;
|
|
|
|
for(auto iter = states.begin(); iter != states.end(); ++iter)
|
|
|
|
{
|
2013-04-04 13:19:28 +00:00
|
|
|
const auto & hs = iter->html_state;
|
|
|
|
double cur_ascent = hs.rise + hs.font_info->ascent * hs.font_size;
|
2013-04-03 17:35:44 +00:00
|
|
|
if(cur_ascent > max_ascent)
|
|
|
|
max_ascent = cur_ascent;
|
2013-04-04 13:19:28 +00:00
|
|
|
|
|
|
|
// set id
|
|
|
|
iter->ids[State::FONT_ID] = hs.font_info->id;
|
|
|
|
iter->ids[State::FONT_SIZE_ID] = renderer->font_size_manager.install(hs.font_size);
|
|
|
|
iter->ids[State::FILL_COLOR_ID] = renderer->fill_color_manager.install(hs.fill_color);
|
|
|
|
iter->ids[State::STROKE_COLOR_ID] = renderer->stroke_color_manager.install(hs.stroke_color);
|
|
|
|
iter->ids[State::LETTER_SPACE_ID] = renderer->letter_space_manager.install(hs.letter_space);
|
|
|
|
iter->ids[State::WORD_SPACE_ID] = renderer->word_space_manager.install(hs.word_space);
|
|
|
|
iter->ids[State::RISE_ID] = renderer->rise_manager.install(hs.rise);
|
2013-04-03 17:35:44 +00:00
|
|
|
iter->hash();
|
|
|
|
}
|
|
|
|
|
2013-04-03 06:04:39 +00:00
|
|
|
long long hid = renderer->height_manager.install(max_ascent);
|
|
|
|
long long lid = renderer->left_manager .install(x);
|
|
|
|
long long bid = renderer->bottom_manager.install(y);
|
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// open <div> for the current text line
|
2013-04-03 06:04:39 +00:00
|
|
|
out << "<div class=\"" << CSS::LINE_CN
|
|
|
|
<< " " << CSS::TRANSFORM_MATRIX_CN << tm_id
|
|
|
|
<< " " << CSS::LEFT_CN << lid
|
|
|
|
<< " " << CSS::HEIGHT_CN << hid
|
2013-04-04 08:28:59 +00:00
|
|
|
<< " " << CSS::BOTTOM_CN << bid;
|
|
|
|
// it will be closed by the first state
|
2013-04-03 06:04:39 +00:00
|
|
|
}
|
2012-09-04 15:33:15 +00:00
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// a special safeguard in the bottom
|
2012-09-06 06:37:09 +00:00
|
|
|
stack.clear();
|
|
|
|
stack.push_back(nullptr);
|
|
|
|
|
2013-04-03 06:04:39 +00:00
|
|
|
//accumulated horizontal offset;
|
|
|
|
double dx = 0;
|
|
|
|
|
2012-09-06 06:37:09 +00:00
|
|
|
// whenever a negative offset appears, we should not pop out that <span>
|
|
|
|
// otherwise the effect of negative margin-left would disappear
|
2012-09-06 11:05:49 +00:00
|
|
|
size_t last_text_pos_with_negative_offset = 0;
|
2012-09-04 15:33:15 +00:00
|
|
|
size_t cur_text_idx = 0;
|
2013-04-03 06:04:39 +00:00
|
|
|
|
|
|
|
auto cur_offset_iter = offsets.begin();
|
|
|
|
for(auto state_iter2 = states.begin(), state_iter1 = state_iter2++;
|
|
|
|
state_iter1 != states.end();
|
|
|
|
++state_iter1, ++state_iter2)
|
2012-09-04 15:33:15 +00:00
|
|
|
{
|
2013-04-03 06:04:39 +00:00
|
|
|
// export current state, find a closest parent
|
|
|
|
{
|
2012-09-06 06:37:09 +00:00
|
|
|
// greedy
|
|
|
|
int best_cost = State::ID_COUNT;
|
|
|
|
// we have a nullptr at the beginning, so no need to check for rend
|
|
|
|
for(auto iter = stack.rbegin(); *iter; ++iter)
|
2012-09-05 07:13:21 +00:00
|
|
|
{
|
2013-04-03 06:04:39 +00:00
|
|
|
int cost = state_iter1->diff(**iter);
|
2012-09-06 06:37:09 +00:00
|
|
|
if(cost < best_cost)
|
|
|
|
{
|
|
|
|
while(stack.back() != *iter)
|
|
|
|
{
|
|
|
|
stack.back()->end(out);
|
|
|
|
stack.pop_back();
|
|
|
|
}
|
|
|
|
best_cost = cost;
|
|
|
|
|
|
|
|
if(best_cost == 0)
|
|
|
|
break;
|
|
|
|
}
|
2012-09-05 07:13:21 +00:00
|
|
|
|
2012-09-06 06:37:09 +00:00
|
|
|
// cannot go further
|
|
|
|
if((*iter)->start_idx <= last_text_pos_with_negative_offset)
|
|
|
|
break;
|
|
|
|
}
|
2013-04-03 17:35:44 +00:00
|
|
|
// export the diff between *state_iter1 and stack.back()
|
2013-04-03 06:04:39 +00:00
|
|
|
state_iter1->begin(out, stack.back());
|
|
|
|
stack.push_back(&*state_iter1);
|
2012-09-04 15:33:15 +00:00
|
|
|
}
|
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// [state_iter1->start_idx, text_idx2) are covered by the current state
|
2013-04-03 06:04:39 +00:00
|
|
|
size_t text_idx2 = (state_iter2 == states.end()) ? text.size() : state_iter2->start_idx;
|
2012-09-04 15:33:15 +00:00
|
|
|
|
2013-04-03 06:10:11 +00:00
|
|
|
// dump all text and offsets before next state
|
2013-04-03 06:04:39 +00:00
|
|
|
while(true)
|
|
|
|
{
|
2013-04-03 17:35:44 +00:00
|
|
|
if((cur_offset_iter != offsets.end())
|
|
|
|
&& (cur_offset_iter->start_idx <= cur_text_idx))
|
2013-03-21 04:18:26 +00:00
|
|
|
{
|
2013-04-03 06:04:39 +00:00
|
|
|
if(cur_offset_iter->start_idx > text_idx2)
|
|
|
|
break;
|
2013-04-03 06:10:11 +00:00
|
|
|
// next is offset
|
2013-04-03 06:04:39 +00:00
|
|
|
double target = cur_offset_iter->width + dx;
|
|
|
|
double actual_offset = 0;
|
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
//ignore near-zero offsets
|
2013-04-03 06:04:39 +00:00
|
|
|
if(abs(target) <= renderer->param->h_eps)
|
2013-03-24 13:42:51 +00:00
|
|
|
{
|
2013-04-03 06:04:39 +00:00
|
|
|
actual_offset = 0;
|
2013-03-24 13:42:51 +00:00
|
|
|
}
|
2013-04-03 06:04:39 +00:00
|
|
|
else
|
2013-03-24 13:42:51 +00:00
|
|
|
{
|
2013-04-03 06:04:39 +00:00
|
|
|
bool done = false;
|
2013-04-03 17:35:44 +00:00
|
|
|
// check if the offset is equivalent to a single ' '
|
2013-04-03 06:04:39 +00:00
|
|
|
if(!(state_iter1->hash_umask & State::umask_by_id(State::WORD_SPACE_ID)))
|
|
|
|
{
|
|
|
|
double space_off = state_iter1->single_space_offset();
|
|
|
|
if(abs(target - space_off) <= renderer->param->h_eps)
|
|
|
|
{
|
|
|
|
Unicode u = ' ';
|
|
|
|
outputUnicodes(out, &u, 1);
|
|
|
|
actual_offset = space_off;
|
|
|
|
done = true;
|
|
|
|
}
|
|
|
|
}
|
2012-09-04 15:33:15 +00:00
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// finally, just dump it
|
2013-04-03 06:04:39 +00:00
|
|
|
if(!done)
|
2013-03-24 13:42:51 +00:00
|
|
|
{
|
2013-04-03 06:04:39 +00:00
|
|
|
long long wid = renderer->whitespace_manager.install(target, &actual_offset);
|
|
|
|
|
|
|
|
if(!equal(actual_offset, 0))
|
|
|
|
{
|
|
|
|
if(is_positive(-actual_offset))
|
|
|
|
last_text_pos_with_negative_offset = cur_text_idx;
|
2012-09-06 06:37:09 +00:00
|
|
|
|
2013-04-03 06:04:39 +00:00
|
|
|
double threshold = state_iter1->em_size() * (renderer->param->space_threshold);
|
2012-09-07 00:39:21 +00:00
|
|
|
|
2013-04-03 06:04:39 +00:00
|
|
|
out << "<span class=\"" << CSS::WHITESPACE_CN
|
|
|
|
<< ' ' << CSS::WHITESPACE_CN << wid << "\">" << (target > (threshold - EPS) ? " " : "") << "</span>";
|
|
|
|
}
|
2013-03-24 13:42:51 +00:00
|
|
|
}
|
|
|
|
}
|
2013-04-03 06:04:39 +00:00
|
|
|
dx = target - actual_offset;
|
|
|
|
++ cur_offset_iter;
|
2013-03-21 04:18:26 +00:00
|
|
|
}
|
2013-04-03 06:04:39 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
if(cur_text_idx >= text_idx2)
|
|
|
|
break;
|
2013-04-03 06:10:11 +00:00
|
|
|
// next is text
|
2013-04-03 06:04:39 +00:00
|
|
|
size_t next_text_idx = text_idx2;
|
|
|
|
if((cur_offset_iter != offsets.end()) && (cur_offset_iter->start_idx) < next_text_idx)
|
|
|
|
next_text_idx = cur_offset_iter->start_idx;
|
|
|
|
outputUnicodes(out, (&text.front()) + cur_text_idx, next_text_idx - cur_text_idx);
|
|
|
|
cur_text_idx = next_text_idx;
|
|
|
|
}
|
|
|
|
}
|
2012-09-04 15:33:15 +00:00
|
|
|
}
|
|
|
|
|
2012-09-05 07:13:21 +00:00
|
|
|
// we have a nullptr in the bottom
|
2012-09-06 06:37:09 +00:00
|
|
|
while(stack.back())
|
2012-09-05 07:13:21 +00:00
|
|
|
{
|
2012-09-06 06:37:09 +00:00
|
|
|
stack.back()->end(out);
|
|
|
|
stack.pop_back();
|
2012-09-05 07:13:21 +00:00
|
|
|
}
|
|
|
|
|
2012-09-04 15:33:15 +00:00
|
|
|
out << "</div>";
|
|
|
|
|
|
|
|
states.clear();
|
|
|
|
offsets.clear();
|
|
|
|
text.clear();
|
|
|
|
}
|
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
/*
|
|
|
|
* Adjust letter space and word space in order to reduce the number of HTML elements
|
|
|
|
* May also unmask word space
|
|
|
|
*/
|
2013-04-03 06:04:39 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::optimize()
|
2013-03-20 15:46:58 +00:00
|
|
|
{
|
2013-04-03 01:06:32 +00:00
|
|
|
if(!(renderer->param->optimize_text))
|
|
|
|
return;
|
|
|
|
|
2013-03-20 15:46:58 +00:00
|
|
|
assert(!states.empty());
|
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
const long long word_space_umask = State::umask_by_id(State::WORD_SPACE_ID);
|
|
|
|
|
2013-04-03 06:04:39 +00:00
|
|
|
// for optimization, we need accurate values
|
2013-04-03 07:44:28 +00:00
|
|
|
auto & ls_manager = renderer->letter_space_manager;
|
2013-04-03 06:04:39 +00:00
|
|
|
auto & ws_manager = renderer->word_space_manager;
|
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
// statistics of widths
|
2013-04-03 08:28:06 +00:00
|
|
|
std::map<double, size_t> width_map;
|
2013-04-03 07:44:28 +00:00
|
|
|
// store optimized offsets
|
|
|
|
std::vector<Offset> new_offsets;
|
|
|
|
new_offsets.reserve(offsets.size());
|
2013-03-30 17:00:04 +00:00
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
auto offset_iter1 = offsets.begin();
|
2013-03-30 17:00:04 +00:00
|
|
|
for(auto state_iter2 = states.begin(), state_iter1 = state_iter2++;
|
|
|
|
state_iter1 != states.end();
|
|
|
|
++state_iter1, ++state_iter2)
|
2013-03-21 04:18:26 +00:00
|
|
|
{
|
2013-04-03 07:44:28 +00:00
|
|
|
const size_t text_idx1 = state_iter1->start_idx;
|
|
|
|
const size_t text_idx2 = (state_iter2 == states.end()) ? text.size() : state_iter2->start_idx;
|
|
|
|
size_t text_count = text_idx2 - text_idx1;
|
2013-03-30 17:00:04 +00:00
|
|
|
|
2013-04-03 17:35:44 +00:00
|
|
|
// there might be some offsets before the first state
|
|
|
|
while((offset_iter1 != offsets.end())
|
|
|
|
&& (offset_iter1->start_idx <= text_idx1))
|
2013-04-03 07:51:55 +00:00
|
|
|
{
|
|
|
|
new_offsets.push_back(*(offset_iter1++));
|
|
|
|
}
|
2013-04-03 17:35:44 +00:00
|
|
|
|
|
|
|
// find the last offset covered by the current state
|
2013-04-03 06:17:27 +00:00
|
|
|
auto offset_iter2 = offset_iter1;
|
|
|
|
for(; (offset_iter2 != offsets.end()) && (offset_iter2->start_idx <= text_idx2); ++offset_iter2) { }
|
2013-04-03 17:35:44 +00:00
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
// There are `offset_count` <span>'s, the target is to reduce this number
|
|
|
|
size_t offset_count = offset_iter2 - offset_iter1;
|
|
|
|
assert(text_count >= offset_count);
|
2013-04-03 17:35:44 +00:00
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
// Optimize letter space
|
2013-04-03 17:35:44 +00:00
|
|
|
// how much letter_space is changed
|
|
|
|
// will be later used for optimizing word space
|
|
|
|
double letter_space_diff = 0;
|
|
|
|
width_map.clear();
|
2013-03-30 17:00:04 +00:00
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
// In some PDF files all letter spaces are implemented as position shifts between each letter
|
|
|
|
// try to simplify it with a proper letter space
|
|
|
|
if(offset_count > 0)
|
2013-03-21 04:18:26 +00:00
|
|
|
{
|
2013-04-03 07:44:28 +00:00
|
|
|
// mark the current letter_space
|
|
|
|
if(text_count > offset_count)
|
|
|
|
width_map.insert(std::make_pair(0, text_count - offset_count));
|
2013-03-30 17:21:14 +00:00
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
for(auto off_iter = offset_iter1; off_iter != offset_iter2; ++off_iter)
|
2013-03-21 04:18:26 +00:00
|
|
|
{
|
2013-04-03 07:44:28 +00:00
|
|
|
const double target = off_iter->width;
|
|
|
|
auto iter = width_map.lower_bound(target-EPS);
|
|
|
|
if((iter != width_map.end()) && (abs(iter->first - target) <= EPS))
|
|
|
|
{
|
|
|
|
++ iter->second;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
width_map.insert(iter, std::make_pair(target, 1));
|
|
|
|
}
|
|
|
|
}
|
2013-04-03 17:35:44 +00:00
|
|
|
|
|
|
|
// TODO snapping the widths may result a better result
|
|
|
|
// e.g. for (-0.7 0.6 -0.2 0.3 10 10), 0 is better than 10
|
2013-04-03 07:44:28 +00:00
|
|
|
double most_used_width = 0;
|
2013-04-03 08:28:06 +00:00
|
|
|
size_t max_count = 0;
|
2013-04-03 07:44:28 +00:00
|
|
|
for(auto iter = width_map.begin(); iter != width_map.end(); ++iter)
|
|
|
|
{
|
|
|
|
if(iter->second > max_count)
|
|
|
|
{
|
|
|
|
most_used_width = iter->first;
|
|
|
|
max_count = iter->second;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// now we would like to adjust letter space to most_used width
|
2013-04-03 10:15:06 +00:00
|
|
|
// we shall apply the optimization only when it can significantly reduce the number of elements
|
|
|
|
if(max_count <= text_count / 2)
|
2013-04-03 07:44:28 +00:00
|
|
|
{
|
|
|
|
// the old value is the best
|
2013-04-03 17:35:44 +00:00
|
|
|
// just copy old offsets
|
2013-04-03 07:44:28 +00:00
|
|
|
new_offsets.insert(new_offsets.end(), offset_iter1, offset_iter2);
|
2013-03-21 04:18:26 +00:00
|
|
|
}
|
2013-03-30 17:00:04 +00:00
|
|
|
else
|
2013-03-21 04:18:26 +00:00
|
|
|
{
|
2013-04-03 07:44:28 +00:00
|
|
|
// install new letter space
|
2013-04-04 13:19:28 +00:00
|
|
|
const double old_ls = state_iter1->html_state.letter_space;
|
|
|
|
state_iter1->ids[State::LETTER_SPACE_ID] = ls_manager.install(old_ls + most_used_width, &(state_iter1->html_state.letter_space));
|
|
|
|
letter_space_diff = old_ls - state_iter1->html_state.letter_space;
|
2013-04-03 07:44:28 +00:00
|
|
|
// update offsets
|
|
|
|
auto off_iter = offset_iter1;
|
|
|
|
// re-count number of offsets
|
|
|
|
offset_count = 0;
|
|
|
|
for(size_t cur_text_idx = text_idx1; cur_text_idx < text_idx2; ++cur_text_idx)
|
|
|
|
{
|
|
|
|
double cur_width = 0;
|
|
|
|
if((off_iter != offset_iter2) && (off_iter->start_idx == cur_text_idx + 1))
|
|
|
|
{
|
|
|
|
cur_width = off_iter->width + letter_space_diff;
|
|
|
|
++off_iter;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
cur_width = letter_space_diff ;
|
|
|
|
}
|
|
|
|
if(!equal(cur_width, 0))
|
|
|
|
{
|
2013-04-03 17:35:44 +00:00
|
|
|
new_offsets.emplace_back(cur_text_idx+1, cur_width);
|
2013-04-03 07:44:28 +00:00
|
|
|
++ offset_count;
|
|
|
|
}
|
|
|
|
}
|
2013-03-21 04:18:26 +00:00
|
|
|
}
|
2013-03-30 17:00:04 +00:00
|
|
|
}
|
2013-03-21 04:18:26 +00:00
|
|
|
|
2013-04-03 07:44:28 +00:00
|
|
|
// Optimize word space
|
|
|
|
|
|
|
|
// In some PDF files all spaces are converted into positionig shift
|
|
|
|
// We may try to change (some of) them to ' ' by adjusting word_space
|
|
|
|
// for now, we cosider only the no-space scenario
|
|
|
|
// which also includes the case when param->space_as_offset is set
|
2013-04-03 17:35:44 +00:00
|
|
|
|
|
|
|
// get the text segment covered by current state (*state_iter1)
|
|
|
|
const auto text_iter1 = text.begin() + text_idx1;
|
|
|
|
const auto text_iter2 = text.begin() + text_idx2;
|
2013-04-03 07:44:28 +00:00
|
|
|
if(find(text_iter1, text_iter2, ' ') == text_iter2)
|
2013-03-30 17:00:04 +00:00
|
|
|
{
|
2013-04-03 07:44:28 +00:00
|
|
|
// if there is not any space, we may change the value of word_space arbitrarily
|
|
|
|
// note that we may only change word space, no offset will be affected
|
|
|
|
// The actual effect will emerge during flushing, where it could be detected that an offset can be optimized as a single space character
|
|
|
|
|
|
|
|
if(offset_count > 0)
|
2013-03-21 04:18:26 +00:00
|
|
|
{
|
2013-04-03 07:44:28 +00:00
|
|
|
double threshold = (state_iter1->em_size()) * (renderer->param->space_threshold);
|
|
|
|
// set word_space for the most frequently used offset
|
|
|
|
double most_used_width = 0;
|
2013-04-03 08:28:06 +00:00
|
|
|
size_t max_count = 0;
|
2013-04-03 07:44:28 +00:00
|
|
|
|
|
|
|
// if offset_count > 0, we must have updated width_map in the previous step
|
|
|
|
// find the most frequent width, with new letter space applied
|
|
|
|
for(auto iter = width_map.begin(); iter != width_map.end(); ++iter)
|
|
|
|
{
|
2013-04-03 17:35:44 +00:00
|
|
|
double fixed_width = iter->first + letter_space_diff; // this is the actual offset in HTML
|
2013-04-03 07:44:28 +00:00
|
|
|
// we don't want to add spaces for tiny gaps, or even negative shifts
|
|
|
|
if((fixed_width >= threshold - EPS) && (iter->second > max_count))
|
|
|
|
{
|
|
|
|
max_count = iter->second;
|
|
|
|
most_used_width = fixed_width;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-04-04 13:19:28 +00:00
|
|
|
state_iter1->html_state.word_space = 0; // clear word_space for single_space_offset
|
2013-04-03 07:44:28 +00:00
|
|
|
double new_word_space = most_used_width - state_iter1->single_space_offset();
|
2013-04-04 13:19:28 +00:00
|
|
|
state_iter1->ids[State::WORD_SPACE_ID] = ws_manager.install(new_word_space, &(state_iter1->html_state.word_space)); // install new word_space
|
2013-04-03 17:35:44 +00:00
|
|
|
state_iter1->hash_umask &= (~word_space_umask); // mark that the word_space is not free
|
2013-04-03 07:44:28 +00:00
|
|
|
}
|
2013-04-03 17:35:44 +00:00
|
|
|
else // there is no offset at all
|
2013-04-03 07:44:28 +00:00
|
|
|
{
|
2013-04-03 17:35:44 +00:00
|
|
|
state_iter1->hash_umask |= word_space_umask; // we just free word_space
|
2013-03-21 04:18:26 +00:00
|
|
|
}
|
|
|
|
}
|
2013-04-03 06:17:27 +00:00
|
|
|
offset_iter1 = offset_iter2;
|
2013-03-30 17:00:04 +00:00
|
|
|
}
|
2013-04-03 07:44:28 +00:00
|
|
|
|
|
|
|
// apply optimization
|
|
|
|
std::swap(offsets, new_offsets);
|
2013-03-20 15:46:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// this state will be converted to a child node of the node of prev_state
|
|
|
|
// dump the difference between previous state
|
|
|
|
// also clone corresponding states
|
2012-12-11 12:48:01 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::State::begin (ostream & out, const State * prev_state)
|
2012-09-05 07:13:21 +00:00
|
|
|
{
|
2013-04-04 08:28:59 +00:00
|
|
|
if(prev_state)
|
2012-09-04 15:33:15 +00:00
|
|
|
{
|
2013-04-04 08:28:59 +00:00
|
|
|
long long cur_mask = 0xff;
|
|
|
|
bool first = true;
|
|
|
|
for(int i = 0; i < ID_COUNT; ++i, cur_mask<<=8)
|
2013-03-20 15:46:58 +00:00
|
|
|
{
|
2013-04-04 08:28:59 +00:00
|
|
|
if(hash_umask & cur_mask) // we don't care about this ID
|
2013-03-20 15:46:58 +00:00
|
|
|
{
|
2013-04-04 08:28:59 +00:00
|
|
|
if (prev_state->hash_umask & cur_mask) // if prev_state do not care about it either
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// otherwise
|
2013-03-20 15:46:58 +00:00
|
|
|
// we have to inherit it
|
|
|
|
ids[i] = prev_state->ids[i];
|
|
|
|
hash_umask &= (~cur_mask);
|
2013-03-25 04:23:29 +00:00
|
|
|
//copy the corresponding value
|
|
|
|
//TODO: this is so ugly
|
|
|
|
switch(i)
|
|
|
|
{
|
2013-04-04 08:28:59 +00:00
|
|
|
case FONT_SIZE_ID:
|
2013-04-04 13:19:28 +00:00
|
|
|
html_state.font_size = prev_state->html_state.font_size;
|
2013-04-04 08:28:59 +00:00
|
|
|
break;
|
|
|
|
case LETTER_SPACE_ID:
|
2013-04-04 13:19:28 +00:00
|
|
|
html_state.letter_space = prev_state->html_state.letter_space;
|
2013-04-04 08:28:59 +00:00
|
|
|
break;
|
|
|
|
case WORD_SPACE_ID:
|
2013-04-04 13:19:28 +00:00
|
|
|
html_state.word_space = prev_state->html_state.word_space;
|
2013-04-04 08:28:59 +00:00
|
|
|
break;
|
|
|
|
case RISE_ID:
|
2013-04-04 13:19:28 +00:00
|
|
|
html_state.rise = prev_state->html_state.rise;
|
2013-04-04 08:28:59 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2013-03-25 04:23:29 +00:00
|
|
|
}
|
2013-03-20 15:46:58 +00:00
|
|
|
}
|
|
|
|
|
2013-04-04 08:28:59 +00:00
|
|
|
// now we care about the ID
|
|
|
|
|
|
|
|
// if the value from prev_state is the same, we don't need to dump it
|
|
|
|
if((!(prev_state->hash_umask & cur_mask)) && (prev_state->ids[i] == ids[i]))
|
|
|
|
continue;
|
2012-09-05 07:13:21 +00:00
|
|
|
|
2013-04-04 08:28:59 +00:00
|
|
|
// so we have to dump it
|
|
|
|
if(first)
|
|
|
|
{
|
|
|
|
out << "<span class=\"";
|
|
|
|
first = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
out << ' ';
|
|
|
|
}
|
|
|
|
|
|
|
|
// out should have hex set
|
|
|
|
out << css_class_names[i];
|
|
|
|
if (ids[i] == -1)
|
|
|
|
out << CSS::INVALID_ID;
|
|
|
|
else
|
|
|
|
out << ids[i];
|
2012-09-05 07:13:21 +00:00
|
|
|
}
|
2013-04-04 08:28:59 +00:00
|
|
|
|
|
|
|
if(first) // we actually just inherit the whole prev_state
|
2012-09-05 07:13:21 +00:00
|
|
|
{
|
2013-04-04 08:28:59 +00:00
|
|
|
need_close = false;
|
2012-09-05 07:13:21 +00:00
|
|
|
}
|
2013-01-31 22:21:57 +00:00
|
|
|
else
|
2013-04-04 08:28:59 +00:00
|
|
|
{
|
|
|
|
out << "\">";
|
|
|
|
need_close = true;
|
|
|
|
}
|
2012-09-06 06:37:09 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2013-04-04 08:28:59 +00:00
|
|
|
// prev_state == nullptr
|
|
|
|
// which means this is the first state of the line
|
|
|
|
// there should be a open pending <div> left there
|
|
|
|
long long cur_mask = 0xff;
|
|
|
|
for(int i = 0; i < ID_COUNT; ++i, cur_mask<<=8)
|
|
|
|
{
|
|
|
|
if(hash_umask & cur_mask) // we don't care about this ID
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// now we care about the ID
|
|
|
|
out << ' ';
|
|
|
|
// out should have hex set
|
|
|
|
out << css_class_names[i];
|
|
|
|
if (ids[i] == -1)
|
|
|
|
out << CSS::INVALID_ID;
|
|
|
|
else
|
|
|
|
out << ids[i];
|
|
|
|
}
|
|
|
|
|
2012-09-06 06:37:09 +00:00
|
|
|
out << "\">";
|
2013-04-04 08:28:59 +00:00
|
|
|
need_close = false;
|
2012-09-06 06:37:09 +00:00
|
|
|
}
|
2012-09-04 15:33:15 +00:00
|
|
|
}
|
|
|
|
|
2012-12-11 12:48:01 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::State::end(ostream & out) const
|
2012-09-05 07:13:21 +00:00
|
|
|
{
|
|
|
|
if(need_close)
|
|
|
|
out << "</span>";
|
|
|
|
}
|
|
|
|
|
2012-12-11 12:48:01 +00:00
|
|
|
void HTMLRenderer::TextLineBuffer::State::hash(void)
|
2012-09-05 07:13:21 +00:00
|
|
|
{
|
|
|
|
hash_value = 0;
|
|
|
|
for(int i = 0; i < ID_COUNT; ++i)
|
|
|
|
{
|
|
|
|
hash_value = (hash_value << 8) | (ids[i] & 0xff);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-12-11 12:48:01 +00:00
|
|
|
int HTMLRenderer::TextLineBuffer::State::diff(const State & s) const
|
2012-09-04 15:33:15 +00:00
|
|
|
{
|
2012-09-05 07:13:21 +00:00
|
|
|
/*
|
|
|
|
* A quick check based on hash_value
|
|
|
|
* it could be wrong when there are more then 256 classes,
|
2012-09-05 08:19:01 +00:00
|
|
|
* in which case the output may not be optimal, but still 'correct' in terms of HTML
|
2012-09-05 07:13:21 +00:00
|
|
|
*/
|
2013-03-20 15:46:58 +00:00
|
|
|
long long common_mask = ~(hash_umask | s.hash_umask);
|
|
|
|
if((hash_value & common_mask) == (s.hash_value & common_mask)) return 0;
|
2012-09-05 07:13:21 +00:00
|
|
|
|
2013-03-20 15:46:58 +00:00
|
|
|
long long cur_mask = 0xff;
|
2012-09-05 07:13:21 +00:00
|
|
|
int d = 0;
|
|
|
|
for(int i = 0; i < ID_COUNT; ++i)
|
2013-03-20 15:46:58 +00:00
|
|
|
{
|
|
|
|
if((common_mask & cur_mask) && (ids[i] != s.ids[i]))
|
2012-09-05 07:13:21 +00:00
|
|
|
++ d;
|
2013-03-20 15:46:58 +00:00
|
|
|
cur_mask <<= 8;
|
|
|
|
}
|
2012-09-05 07:13:21 +00:00
|
|
|
return d;
|
2012-09-04 15:33:15 +00:00
|
|
|
}
|
|
|
|
|
2013-03-21 04:18:26 +00:00
|
|
|
double HTMLRenderer::TextLineBuffer::State::single_space_offset(void) const
|
|
|
|
{
|
2013-04-04 13:19:28 +00:00
|
|
|
return html_state.word_space + html_state.letter_space + html_state.font_info->space_width * html_state.font_size;
|
2013-03-21 04:18:26 +00:00
|
|
|
}
|
|
|
|
|
2013-03-30 17:21:14 +00:00
|
|
|
double HTMLRenderer::TextLineBuffer::State::em_size(void) const
|
|
|
|
{
|
2013-04-04 13:19:28 +00:00
|
|
|
return html_state.font_size * (html_state.font_info->ascent - html_state.font_info->descent);
|
2013-03-30 17:21:14 +00:00
|
|
|
}
|
|
|
|
|
2013-03-25 04:23:29 +00:00
|
|
|
long long HTMLRenderer::TextLineBuffer::State::umask_by_id(int id)
|
|
|
|
{
|
|
|
|
return (((long long)0xff) << (8*id));
|
|
|
|
}
|
|
|
|
|
2013-02-05 10:19:25 +00:00
|
|
|
// the order should be the same as in the enum
|
|
|
|
const char * const HTMLRenderer::TextLineBuffer::State::css_class_names [] = {
|
2013-02-28 07:59:14 +00:00
|
|
|
CSS::FONT_FAMILY_CN,
|
2013-02-05 10:19:25 +00:00
|
|
|
CSS::FONT_SIZE_CN,
|
|
|
|
CSS::FILL_COLOR_CN,
|
|
|
|
CSS::STROKE_COLOR_CN,
|
|
|
|
CSS::LETTER_SPACE_CN,
|
|
|
|
CSS::WORD_SPACE_CN,
|
|
|
|
CSS::RISE_CN
|
|
|
|
};
|
|
|
|
|
2012-09-12 15:26:14 +00:00
|
|
|
} //namespace pdf2htmlEX
|