index f6b9688bb17ca6659b97a54be8239a73b9fe01a1..77e21ef56e916fd913d8ad66a26295fa2063ead2 100644 (file)
/** for y= attributes in tspan elements et al, we do the adjustment by moving each
glyph individually by this number. The spec means that this is maintained across
- paragraphs. */
+ paragraphs.
+
+ To do non-flow text layout, only the first "y" attribute is normally used. If there is only one
+ "y" attribute in a <tspan> other than the first <tspan>, it is ignored. This allows Inkscape to
+ insert a new line anywhere. On output, the Inkscape determined "y" is written out so other SVG
+ viewers know where to place the <tspans>.
+ */
+
double _y_offset;
/** to stop pango from hinting its output, the font factory creates all fonts very large.
unsigned input_index; /// index into Layout::_input_stream
Glib::ustring::const_iterator input_stream_first_character;
double font_size;
- LineHeight line_height;
+ LineHeight line_height; /// This is not the CSS line-height attribute!
double line_height_multiplier; /// calculated from the font-height css property
+ double baseline_shift; /// calculated from the baseline-shift css property
unsigned text_bytes;
unsigned char_index_in_para; /// the index of the first character in this span in the paragraph, for looking up char_attributes
SVGLength x, y, dx, dy, rotate; // these are reoriented copies of the <tspan> attributes. We change span when we encounter one.
unsigned whitespace_count;
bool ends_with_whitespace;
double each_whitespace_width;
+ double letter_spacing; // Save so we can subtract from width at end of line (for center justification)
+ double word_spacing;
void setZero();
};
- /** The definition of a chunk used here is the same as that used in Layout. */
+ /** The definition of a chunk used here is the same as that used in Layout:
+ A collection of contiguous broken spans on the same line. (One chunk per line
+ unless shape splits line into several sections... then one chunk per section. */
struct ChunkInfo {
std::vector<BrokenSpan> broken_spans;
double scanrun_width;
span->end.increment();
- if (span->width > maximum_width && !char_attributes.is_white) { // whitespaces don't matter, we can put as many as we want at eol
+ // Width should not include letter_spacing (or word_spacing) after last letter at end of line.
+ // word_spacing is attached to white space that is already removed from line end (?)
+ double test_width = span->width - text_source->style->letter_spacing.computed;
+
+ // Save letter_spacing and word_spacing for subtraction later if span is last span in line.
+ span->letter_spacing = text_source->style->letter_spacing.computed;
+ span->word_spacing = text_source->style->word_spacing.computed;
+
+ if (test_width > maximum_width && !char_attributes.is_white) { // whitespaces don't matter, we can put as many as we want at eol
TRACE(("span %d exceeded scanrun; width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count));
return false;
}
} while (span->end.char_byte != 0); // while we haven't wrapped to the next span
+
TRACE(("fitted span %d width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count));
return true;
}
case RIGHT:
return it_chunk->x - it_chunk->text_width;
case CENTER:
- return it_chunk->x - it_chunk->text_width / 2;
+ return it_chunk->x - it_chunk->text_width/ 2;
}
}
*/
void _outputLine(ParagraphInfo const ¶, LineHeight const &line_height, std::vector<ChunkInfo> const &chunk_info)
{
+ TRACE(("Start _outputLine\n"));
if (chunk_info.empty()) {
TRACE(("line too short to fit anything on it, go to next\n"));
return;
// add the chunk to the list
Layout::Chunk new_chunk;
new_chunk.in_line = _flow._lines.size() - 1;
+ TRACE((" New chunk: in_line: %d\n", new_chunk.in_line));
new_chunk.left_x = _getChunkLeftWithAlignment(para, it_chunk, &add_to_each_whitespace);
+
// we may also have y move orders to deal with here (dx, dy and rotate are done per span)
- if (!it_chunk->broken_spans.empty() // this one only happens for empty paragraphs
- && it_chunk->broken_spans.front().start.char_byte == 0
- && it_chunk->broken_spans.front().start.iter_span->y._set) {
- // if this is the start of a line, we should change the baseline rather than each glyph individually
- if (_flow._characters.empty() || _flow._characters.back().chunk(&_flow).in_line != _flow._lines.size() - 1) {
- new_line.baseline_y = it_chunk->broken_spans.front().start.iter_span->y.computed;
- _flow._lines.back().baseline_y = new_line.baseline_y;
+
+ // Comment updated: 23 July 2010:
+ // We must handle two cases:
+ //
+ // 1. Inkscape SVG where the first line is placed by the read-in "y" value and the
+ // rest are determined by 'font-size' and 'line-height' (and not by any
+ // y-kerning). <tspan>s in this case are marked by sodipodi:role="line". This
+ // allows new lines to be inserted in the middle of a <text> object. On output,
+ // new "y" values are calculated for each <tspan> that represents a new line. Line
+ // spacing is already handled by the calling routine.
+ //
+ // 2. Plain SVG where each <text> or <tspan> is placed by its own "x" and "y" values.
+ // Note that in this case Inkscape treats each <text> object with any included
+ // <tspan>s as a single line of text. This can be confusing in the code below.
+
+ if (!it_chunk->broken_spans.empty() // Not empty paragraph
+ && it_chunk->broken_spans.front().start.char_byte == 0 ) { // Beginning of unbroken span
+
+ // If empty or new line (sodipode:role="line")
+ if( _flow._characters.empty() ||
+ _flow._characters.back().chunk(&_flow).in_line != _flow._lines.size() - 1) {
+
+ // This is the Inkscape SVG case.
+ //
+ // If <tspan> "y" attribute is set, use it (initial "y" attributes in
+ // <tspans> other than the first have already been stripped for <tspans>
+ // marked with role="line", see sp-text.cpp: SPText::_buildLayoutInput).
+ if( it_chunk->broken_spans.front().start.iter_span->y._set ) {
+
+ // Use set "y" attribute
+ new_line.baseline_y = it_chunk->broken_spans.front().start.iter_span->y.computed;
+
+ // Save baseline
+ _flow._lines.back().baseline_y = new_line.baseline_y;
+
+ // Save new <tspan> y coordinate
+ _scanline_maker->setNewYCoordinate(new_line.baseline_y - line_height.ascent);
+
+ }
+
+ // Reset relative y_offset ("dy" attribute is relative but should be reset at
+ // the beginning of each line since each line will have a new "y" written out.)
_y_offset = 0.0;
- _scanline_maker->setNewYCoordinate(new_line.baseline_y - line_height.ascent);
- } else
- _y_offset = it_chunk->broken_spans.front().start.iter_span->y.computed - new_line.baseline_y;
+
+ } else {
+
+ // This is the plain SVG case
+ //
+ // "x" and "y" are used to place text, simulating lines as necessary
+ if( it_chunk->broken_spans.front().start.iter_span->y._set ) {
+ _y_offset = it_chunk->broken_spans.front().start.iter_span->y.computed - new_line.baseline_y;
+ }
+ }
}
_flow._chunks.push_back(new_chunk);
UnbrokenSpan const &unbroken_span = *it_span->start.iter_span;
if (it_span->start.char_byte == 0) {
- // start of an unbroken span, we might have dx, dy or rotate still to process (x and y are done per chunk)
+ // Start of an unbroken span, we might have dx, dy or rotate still to process
+ // (x and y are done per chunk)
if (unbroken_span.dx._set) x += unbroken_span.dx.computed;
if (unbroken_span.dy._set) _y_offset += unbroken_span.dy.computed;
if (unbroken_span.rotate._set) glyph_rotate = unbroken_span.rotate.computed * (M_PI/180);
new_span.in_chunk = _flow._chunks.size() - 1;
new_span.line_height = unbroken_span.line_height;
new_span.in_input_stream_item = unbroken_span.input_index;
- new_span.baseline_shift = _y_offset;
+ new_span.baseline_shift = 0.0;
new_span.block_progression = _block_progression;
if ((_flow._input_stream[unbroken_span.input_index]->Type() == TEXT_SOURCE) && (new_span.font = para.pango_items[unbroken_span.pango_item_index].font))
{
if (_block_progression == LEFT_TO_RIGHT || _block_progression == RIGHT_TO_LEFT) {
new_glyph.x = x + unbroken_span.glyph_string->glyphs[glyph_index].geometry.x_offset * font_size_multiplier + new_span.line_height.ascent;
- new_glyph.y = _y_offset + (unbroken_span.glyph_string->glyphs[glyph_index].geometry.y_offset - unbroken_span.glyph_string->glyphs[glyph_index].geometry.width * 0.5) * font_size_multiplier;
+ new_glyph.y = _y_offset -
+ unbroken_span.baseline_shift +
+ (unbroken_span.glyph_string->glyphs[glyph_index].geometry.y_offset -
+ unbroken_span.glyph_string->glyphs[glyph_index].geometry.width * 0.5) * font_size_multiplier;
new_glyph.width = new_span.font_size * para.pango_items[unbroken_span.pango_item_index].font->Advance(unbroken_span.glyph_string->glyphs[glyph_index].glyph, true);
} else {
new_glyph.x = x + unbroken_span.glyph_string->glyphs[glyph_index].geometry.x_offset * font_size_multiplier;
- new_glyph.y = _y_offset + unbroken_span.glyph_string->glyphs[glyph_index].geometry.y_offset * font_size_multiplier;
+ new_glyph.y = _y_offset -
+ unbroken_span.baseline_shift +
+ unbroken_span.glyph_string->glyphs[glyph_index].geometry.y_offset * font_size_multiplier;
new_glyph.width = unbroken_span.glyph_string->glyphs[glyph_index].geometry.width * font_size_multiplier;
if ((new_glyph.width == 0) && (para.pango_items[unbroken_span.pango_item_index].font))
new_glyph.width = new_span.font_size * para.pango_items[unbroken_span.pango_item_index].font->Advance(unbroken_span.glyph_string->glyphs[glyph_index].glyph, false);
@@ -936,6 +1008,7 @@ void Layout::Calculator::_computeFontLineHeight(font_instance *font, double font
*line_height_multiplier = LINE_HEIGHT_NORMAL * font_size / line_height->total();
}
+
/**
* Split the paragraph into spans. Also call pango_shape() on them.
*
}
} else if (_flow._input_stream[input_index]->Type() == TEXT_SOURCE && pango_item_index < para->pango_items.size()) {
Layout::InputStreamTextSource const *text_source = static_cast<Layout::InputStreamTextSource const *>(_flow._input_stream[input_index]);
+
unsigned char_index_in_source = 0;
unsigned span_start_byte_in_source = 0;
unsigned const text_source_bytes = ( text_source->text_end.base()
- text_source->text_begin.base()
- span_start_byte_in_source );
+ TRACE(("New Span\n"));
UnbrokenSpan new_span;
new_span.text_bytes = std::min(text_source_bytes, pango_item_bytes);
new_span.input_stream_first_character = Glib::ustring::const_iterator(text_source->text_begin.base() + span_start_byte_in_source);
}
new_span.pango_item_index = pango_item_index;
_computeFontLineHeight(para->pango_items[pango_item_index].font, new_span.font_size, text_source->style, &new_span.line_height, &new_span.line_height_multiplier);
+
+ // At some point we may want to calculate baseline_shift here (to take advantage
+ // of otm features like superscript baseline), but for now we use style baseline_shift.
+ new_span.baseline_shift = text_source->style->baseline_shift.computed;
+
// TODO: metrics for vertical text
TRACE(("add text span %d \"%s\"\n", para->unbroken_spans.size(), text_source->text->raw().substr(span_start_byte_in_source, new_span.text_bytes).c_str()));
TRACE((" %d glyphs\n", new_span.glyph_string->num_glyphs));
UnbrokenSpanPosition span_pos;
for( ; ; ) {
std::vector<ScanlineMaker::ScanRun> scan_runs;
- scan_runs = _scanline_maker->makeScanline(*line_height);
+ scan_runs = _scanline_maker->makeScanline(*line_height); // Only one line with "InfiniteScanlineMaker
while (scan_runs.empty()) {
+ // Only used by ShapeScanlineMaker
if (!_goToNextWrapShape()) return false; // no more shapes to wrap in to
scan_runs = _scanline_maker->makeScanline(*line_height);
}
chunk_info->back().whitespace_count--;
}
+ if (!chunk_info->empty() && !chunk_info->back().broken_spans.empty() ) {
+ // for justification we need to discard line-spacing and word-spacing at end of the chunk
+ chunk_info->back().broken_spans.back().width -= chunk_info->back().broken_spans.back().letter_spacing;
+ chunk_info->back().text_width -= chunk_info->back().broken_spans.back().letter_spacing;
+ TRACE(("width after subtracting last letter_spacing: %f\n", chunk_info->back().broken_spans.back().width));
+ }
+
return true;
}
_createFirstScanlineMaker();
ParagraphInfo para;
- LineHeight line_height; // needs to be maintained across paragraphs to be able to deal with blank paras (this is wrong)
+ LineHeight line_height; // needs to be maintained across paragraphs to be able to deal with blank paras
for(para.first_input_index = 0 ; para.first_input_index < _flow._input_stream.size() ; ) {
// jump to the next wrap shape if this is a SHAPE_BREAK control code
if (_flow._input_stream[para.first_input_index]->Type() == CONTROL_CODE) {
break; // out of shapes to wrap in to
_outputLine(para, line_height, line_chunk_info);
- _scanline_maker->completeLine();
+ _scanline_maker->completeLine(); // Increments y by line height
} while (span_pos.iter_span != para.unbroken_spans.end());
TRACE(("para %d end\n\n", _flow._paragraphs.size() - 1));