/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.gvt.renderer;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import org.apache.batik.gvt.TextNode;
import org.apache.batik.gvt.TextPainter;
import org.apache.batik.gvt.font.FontFamilyResolver;
import org.apache.batik.gvt.font.GVTFont;
import org.apache.batik.gvt.font.GVTFontFamily;
import org.apache.batik.gvt.font.GVTGlyphMetrics;
import org.apache.batik.gvt.font.UnresolvedFontFamily;
import org.apache.batik.gvt.text.AttributedCharacterSpanIterator;
import org.apache.batik.gvt.text.BidiAttributedCharacterIterator;
import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;
import org.apache.batik.gvt.text.Mark;
import org.apache.batik.gvt.text.TextHit;
import org.apache.batik.gvt.text.TextPath;
import org.apache.batik.gvt.text.TextSpanLayout;
/**
* More sophisticated implementation of TextPainter which
* renders the attributed character iterator of a <tt>TextNode</tt>.
* <em>StrokingTextPainter includes support for stroke, fill, opacity,
* text-decoration, and other attributes.</em>
*
* @see org.apache.batik.gvt.TextPainter
* @see org.apache.batik.gvt.text.GVTAttributedCharacterIterator
*
* @author <a href="bill.haneman@ireland.sun.com>Bill Haneman</a>
* @version $Id: StrokingTextPainter.java,v 1.45 2003/07/09 02:10:10 deweese Exp $
*/
public class StrokingTextPainter extends BasicTextPainter {
public static final
AttributedCharacterIterator.Attribute FLOW_REGIONS =
GVTAttributedCharacterIterator.TextAttribute.FLOW_REGIONS;
public static final
AttributedCharacterIterator.Attribute FLOW_PARAGRAPH =
GVTAttributedCharacterIterator.TextAttribute.FLOW_PARAGRAPH;
public static final
AttributedCharacterIterator.Attribute TEXT_COMPOUND_DELIMITER
= GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_DELIMITER;
public static final
AttributedCharacterIterator.Attribute GVT_FONT
= GVTAttributedCharacterIterator.TextAttribute.GVT_FONT;
public static final
AttributedCharacterIterator.Attribute GVT_FONT_FAMILIES
= GVTAttributedCharacterIterator.TextAttribute.GVT_FONT_FAMILIES;
public static final
AttributedCharacterIterator.Attribute BIDI_LEVEL
= GVTAttributedCharacterIterator.TextAttribute.BIDI_LEVEL;
public static final
AttributedCharacterIterator.Attribute XPOS
= GVTAttributedCharacterIterator.TextAttribute.X;
public static final
AttributedCharacterIterator.Attribute YPOS
= GVTAttributedCharacterIterator.TextAttribute.Y;
public static final
AttributedCharacterIterator.Attribute TEXTPATH
= GVTAttributedCharacterIterator.TextAttribute.TEXTPATH;
private static final AttributedCharacterIterator.Attribute WRITING_MODE
= GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE;
private static final Integer WRITING_MODE_TTB
= GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE_TTB;
private static final Integer WRITING_MODE_RTL
= GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE_RTL;
public static final
AttributedCharacterIterator.Attribute ANCHOR_TYPE
= GVTAttributedCharacterIterator.TextAttribute.ANCHOR_TYPE;
public static final Integer ADJUST_SPACING =
GVTAttributedCharacterIterator.TextAttribute.ADJUST_SPACING;
public static final Integer ADJUST_ALL =
GVTAttributedCharacterIterator.TextAttribute.ADJUST_ALL;
public static final GVTAttributedCharacterIterator.TextAttribute ALT_GLYPH_HANDLER =
GVTAttributedCharacterIterator.TextAttribute.ALT_GLYPH_HANDLER;
static Set extendedAtts = new HashSet();
static {
extendedAtts.add(FLOW_PARAGRAPH);
extendedAtts.add(TEXT_COMPOUND_DELIMITER);
extendedAtts.add(GVT_FONT);
extendedAtts.add(BIDI_LEVEL);
}
/**
* A unique instance of this class.
*/
protected static TextPainter singleton = new StrokingTextPainter();
/**
* Returns a unique instance of this class.
*/
public static TextPainter getInstance() {
return singleton;
}
/**
* Paints the specified text node using the specified Graphics2D.
*
* @param node the text node to paint
* @param g2d the Graphics2D to use
*/
public void paint(TextNode node, Graphics2D g2d) {
AttributedCharacterIterator aci;
aci = node.getAttributedCharacterIterator();
List textRuns = getTextRuns(node, aci);
// draw the underline and overline first, then the actual text
// and finally the strikethrough
paintDecorations(textRuns, g2d, TextSpanLayout.DECORATION_UNDERLINE);
paintDecorations(textRuns, g2d, TextSpanLayout.DECORATION_OVERLINE);
paintTextRuns(textRuns, g2d);
paintDecorations
(textRuns, g2d, TextSpanLayout.DECORATION_STRIKETHROUGH);
}
private void printAttrs(AttributedCharacterIterator aci) {
aci.first();
int start = aci.getBeginIndex();
System.out.print("AttrRuns: ");
while (aci.current() != CharacterIterator.DONE) {
int end = aci.getRunLimit();
System.out.print(""+(end-start)+", ");
aci.setIndex(end);
start = end;
}
System.out.println("");
}
// static long reorderTime, fontMatchingTime, layoutTime;
public List getTextRuns(TextNode node, AttributedCharacterIterator aci) {
List textRuns = node.getTextRuns();
if (textRuns != null) {
return textRuns;
}
AttributedCharacterIterator[] chunkACIs = getTextChunkACIs(aci);
int [][] chunkCharMaps = new int[chunkACIs.length][];
// long t0, t1;
// t0 = System.currentTimeMillis();
// reorder each chunk ACI for bidi text
int chunkStart = aci.getBeginIndex();
for (int i = 0; i < chunkACIs.length; i++) {
BidiAttributedCharacterIterator iter;
iter = new BidiAttributedCharacterIterator
(chunkACIs[i], fontRenderContext, chunkStart);
chunkACIs [i] = iter;
chunkCharMaps[i] = iter.getCharMap();
// t1 = System.currentTimeMillis();
// reorderTime += t1-t0;
// t0=t1;
chunkACIs [i] = createModifiedACIForFontMatching
(node, chunkACIs[i]);
chunkStart += (chunkACIs[i].getEndIndex()-
chunkACIs[i].getBeginIndex());
// t1 = System.currentTimeMillis();
// fontMatchingTime += t1-t0;
// t0 = t1;
}
// create text runs for each chunk and add them to the list
textRuns = new ArrayList();
TextChunk chunk, prevChunk=null;
int currentChunk = 0;
Point2D location = node.getLocation();
do {
// Text Chunks contain one or more TextRuns, which they
// create from the ACI.
chunkACIs[currentChunk].first();
chunk = getTextChunk(node,
chunkACIs[currentChunk],
chunkCharMaps[currentChunk],
textRuns,
prevChunk);
// Adjust according to text-anchor property value
chunkACIs[currentChunk].first();
if (chunk != null) {
location = adjustChunkOffsets(location, textRuns, chunk);
}
prevChunk = chunk;
currentChunk++;
} while (chunk != null && currentChunk < chunkACIs.length);
aci.first();
List rgns = (List)aci.getAttribute(FLOW_REGIONS);
if (rgns != null) {
Iterator i = textRuns.iterator();
List chunkLayouts = new ArrayList();
TextRun tr = (TextRun)i.next();
List layouts = new ArrayList();
chunkLayouts.add(layouts);
layouts.add(tr.getLayout());
while (i.hasNext()) {
tr = (TextRun)i.next();
if (tr.isFirstRunInChunk()) {
layouts = new ArrayList();
chunkLayouts.add(layouts);
}
layouts.add(tr.getLayout());
}
org.apache.batik.gvt.text.GlyphLayout.textWrapTextChunk
(chunkACIs, chunkLayouts, rgns);
}
// t1 = System.currentTimeMillis();
// layoutTime += t1-t0;
// System.out.println("Reorder: " + reorderTime + " FontMatching: " + fontMatchingTime + " Layout: " + layoutTime);
// cache the textRuns so don't need to recalculate
node.setTextRuns(textRuns);
return textRuns;
}
/**
* Returns an array of ACIs, one for each text chunk within the given
* text node.
*/
private AttributedCharacterIterator[] getTextChunkACIs
(AttributedCharacterIterator aci) {
List aciList = new ArrayList();
int chunkStartIndex = aci.getBeginIndex();
aci.first();
Object writingMode = aci.getAttribute(WRITING_MODE);
boolean vertical = (writingMode == WRITING_MODE_TTB);
while (aci.setIndex(chunkStartIndex) != CharacterIterator.DONE) {
TextPath prevTextPath = null;
for (int start=chunkStartIndex, end=0;
aci.setIndex(start) != CharacterIterator.DONE; start=end) {
TextPath textPath = (TextPath) aci.getAttribute(TEXTPATH);
if (start != chunkStartIndex) {
// If we aren't the first composite in a chunck see
// if we need to form a new TextChunk...
// We only create new chunks when given an absolute
// location in progression direction [Spec says
// to do it for either but this doesn't make sense].
if (vertical) {
Float runY = (Float) aci.getAttribute(YPOS);
// Check for absolute location in layout direction.
if ((runY != null) && !runY.isNaN())
break; // If so end of chunk...
} else {
Float runX = (Float) aci.getAttribute(XPOS);
// Check for absolute location in layout direction.
if ((runX != null) && !runX.isNaN())
break; // If so end of chunk...
}
// Do additional check for the start of a textPath
if ((prevTextPath == null) && (textPath != null))
break; // If so end of chunk.
// Form a new chunk at the end of a text path.
// [ This is not mentioned in the spec but makes
// sense].
if ((prevTextPath != null) && (textPath == null))
break;
}
prevTextPath = textPath;
// We need to text chunk based on flow paragraphs.
// This prevents BIDI reordering across paragraphs.
if (aci.getAttribute(FLOW_PARAGRAPH) != null) {
end = aci.getRunLimit(FLOW_PARAGRAPH);
// System.out.println("End: " + end);
aci.setIndex(end);
break;
}
// find end of compound.
end = aci.getRunLimit(TEXT_COMPOUND_DELIMITER);
if (start != chunkStartIndex)
// If we aren't starting a new chunk then we know
// we don't have any absolute positioning so there
// is no reason to consider spliting the chunk further.
continue;
// We are starting a new chunk
// So check if we need to split it further...
TextNode.Anchor anchor;
anchor = (TextNode.Anchor) aci.getAttribute(ANCHOR_TYPE);
if (anchor == TextNode.Anchor.START)
continue;
// We need to check if we have a list of X's & Y's if
// so we need to create TextChunk ACI's for each char
// (technically we have to do this for
// text-anchor:start as well but since that is the
// default layout it doesn't matter in that case.
if (vertical) {
Float runY = (Float) aci.getAttribute(YPOS);
// Check for absolute location in layout direction.
if ((runY == null) || runY.isNaN())
// No absolute positioning in text direction continue
continue;
} else {
Float runX = (Float) aci.getAttribute(XPOS);
// Check for absolute location in layout direction.
if ((runX == null) || runX.isNaN())
// No absolute positioning in text direction continue
continue;
}
// Splitting the compound into one char chunks until
// we run out of Xs.
for (int i=start+1; i< end; i++) {
aci.setIndex(i);
if (vertical) {
Float runY = (Float) aci.getAttribute(YPOS);
if ((runY == null) || runY.isNaN())
break;
} else {
Float runX = (Float) aci.getAttribute(XPOS);
if ((runX == null) || runX.isNaN())
break;
}
aciList.add(new AttributedCharacterSpanIterator
(aci, i-1, i));
chunkStartIndex = i;
}
}
// found the end of a text chunck
int chunkEndIndex = aci.getIndex();
// System.out.println("Bounds: " + chunkStartIndex +
// "," + chunkEndIndex);
aciList.add(new AttributedCharacterSpanIterator
(aci, chunkStartIndex, chunkEndIndex));
chunkStartIndex = chunkEndIndex;
}
// copy the text chunks into an array
AttributedCharacterIterator[] aciArray =
new AttributedCharacterIterator[aciList.size()];
Iterator iter = aciList.iterator();
for (int i=0; iter.hasNext(); ++i) {
aciArray[i] = (AttributedCharacterIterator)iter.next();
}
return aciArray;
}
/**
* Returns a new AttributedCharacterIterator that contains resolved GVTFont
* attributes. This is then used when creating the text runs so that the
* text can be split on changes of font as well as tspans and trefs.
*
* @param node The text node that the aci belongs to.
* @param aci The aci to be modified should already be split into
* text chunks.
*
* @return The new modified aci.
*/
private AttributedCharacterIterator createModifiedACIForFontMatching
(TextNode node, AttributedCharacterIterator aci) {
aci.first();
AttributedString as = null;
int asOff = 0;
int begin = aci.getBeginIndex();
boolean moreChunks = true;
int start, end = aci.getRunStart(TEXT_COMPOUND_DELIMITER);
while (moreChunks) {
start = end;
end = aci.getRunLimit(TEXT_COMPOUND_DELIMITER);
int aciLength = end-start;
Vector fontFamilies;
fontFamilies = (Vector)aci.getAttributes().get(GVT_FONT_FAMILIES);
if (fontFamilies == null ) {
// no font families set this chunk so just increment...
asOff += aciLength;
moreChunks = (aci.setIndex(end) != aci.DONE);
continue;
}
// resolve any unresolved font families in the list
List resolvedFontFamilies = new ArrayList(fontFamilies.size());
for (int i = 0; i < fontFamilies.size(); i++) {
GVTFontFamily fontFamily = (GVTFontFamily)fontFamilies.get(i);
if (fontFamily instanceof UnresolvedFontFamily) {
fontFamily = FontFamilyResolver.resolve
((UnresolvedFontFamily)fontFamily);
}
if (fontFamily != null) // Add font family if resolved
resolvedFontFamilies.add(fontFamily);
}
// if could not resolve at least one of the fontFamilies
// then use the default font
if (resolvedFontFamilies.size() == 0) {
resolvedFontFamilies.add(FontFamilyResolver.defaultFont);
}
// create a list of fonts of the correct size
float fontSize = 12;
Float fsFloat = (Float)aci.getAttributes().get(TextAttribute.SIZE);
if (fsFloat != null) {
fontSize = fsFloat.floatValue();
}
// now for each char or group of chars in the string,
// find a font that can display it.
boolean[] fontAssigned = new boolean[aciLength];
if (as == null)
as = new AttributedString(aci);
GVTFont defaultFont = null;;
int numSet=0;
int firstUnset=start;
boolean firstUnsetSet;
for (int i = 0; i < resolvedFontFamilies.size(); i++) {
// assign this font to all characters it can display if it has
// not already been assigned
int currentIndex = firstUnset;
firstUnsetSet = false;
aci.setIndex(currentIndex);
GVTFontFamily ff;
ff = ((GVTFontFamily)resolvedFontFamilies.get(i));
GVTFont font = ff.deriveFont(fontSize, aci);
if (defaultFont == null)
defaultFont = font;
while (currentIndex < end) {
int displayUpToIndex = font.canDisplayUpTo
(aci, currentIndex, end);
Object altGlyphElement = aci.getAttributes().get(ALT_GLYPH_HANDLER);
if ( altGlyphElement != null ){
//found all the glyph to be displayed
//consider the font matching done
displayUpToIndex = -1;
}
if (displayUpToIndex == -1) {
// Can handle the whole thing...
displayUpToIndex = end;
}
if (displayUpToIndex <= currentIndex) {
if (!firstUnsetSet) {
firstUnset = currentIndex;
firstUnsetSet = true;
}
// couldn't display the current char
currentIndex++;
} else {
// could display some text, so for each
// char it can display, if char not already
// assigned a font, assign this font to it
int runStart = -1;
for (int j = currentIndex; j < displayUpToIndex; j++) {
if (fontAssigned[j - start]) {
if (runStart != -1) {
// System.out.println("Font 1: " + font);
as.addAttribute(GVT_FONT, font,
runStart-begin, j-begin);
runStart=-1;
}
} else {
if (runStart == -1)
runStart = j;
}
fontAssigned[j - start] = true;
numSet++;
}
if (runStart != -1) {
// System.out.println("Font 2: " + font);
as.addAttribute(GVT_FONT, font,
runStart-begin,
displayUpToIndex-begin);
}
// set currentIndex to be one after the char
// that couldn't display
currentIndex = displayUpToIndex+1;
}
}
if (numSet == aciLength) // all chars have font set;
break;
}
// assign the first font to any chars haven't alreay been assigned
int runStart = -1;
GVTFontFamily prevFF = null;
GVTFont prevF = defaultFont;
for (int i = 0; i < aciLength; i++) {
if (fontAssigned[i]) {
if (runStart != -1) {
// System.out.println("Font 3: " + prevF);
as.addAttribute(GVT_FONT, prevF,
runStart+asOff, i+asOff);
runStart = -1;
prevF = null;
prevFF = null;
}
} else {
char c = aci.setIndex(start+i);
GVTFontFamily fontFamily;
fontFamily = FontFamilyResolver.getFamilyThatCanDisplay(c);
// fontFamily = (GVTFontFamily)resolvedFontFamilies.get(0);
if (runStart == -1) {
// Starting a new run...
runStart = i;
prevFF = fontFamily;
if (prevFF == null)
prevF = defaultFont;
else
prevF = fontFamily.deriveFont(fontSize, aci);
} else if (prevFF != fontFamily) {
// Font family changed...
// System.out.println("Font 4: " + prevF);
as.addAttribute(GVT_FONT, prevF,
runStart+asOff, i+asOff);
runStart = i;
prevFF = fontFamily;
if (prevFF == null)
prevF = defaultFont;
else
prevF = fontFamily.deriveFont(fontSize, aci);
}
}
}
if (runStart != -1) {
// System.out.println("Font 5: " + prevF);
as.addAttribute(GVT_FONT, prevF,
runStart+asOff, aciLength+asOff);
}
asOff += aciLength;
if (aci.setIndex(end) == aci.DONE) {
moreChunks = false;
}
start = end;
}
if (as != null)
return as.getIterator();
// Didn't do anything return original ACI
return aci;
}
private TextChunk getTextChunk(TextNode node,
AttributedCharacterIterator aci,
int [] charMap,
List textRuns,
TextChunk prevChunk) {
int beginChunk = 0;
if (prevChunk != null)
beginChunk = prevChunk.end;
int endChunk = beginChunk;
int begin = aci.getIndex();
// System.out.println("New Chunk");
if (aci.current() == CharacterIterator.DONE)
return null;
// we now lay all aci's out at 0,0 then move them
// when we adjust the chunk offsets.
Point2D.Float offset = new Point2D.Float(0,0);
Point2D.Float advance = new Point2D.Float(0,0);
boolean isChunkStart = true;
TextSpanLayout layout = null;
do {
int start = aci.getRunStart(extendedAtts);
int end = aci.getRunLimit(extendedAtts);
AttributedCharacterIterator runaci;
runaci = new AttributedCharacterSpanIterator(aci, start, end);
int [] subCharMap = new int[end-start];
for (int i=0; i<subCharMap.length; i++) {
subCharMap[i] = charMap[i+start-begin];
}
FontRenderContext frc = fontRenderContext;
RenderingHints rh = node.getRenderingHints();
// Check for optimizeSpeed, optimizeLegibility
// in these cases setup hintedFRC
if ((rh != null) &&
(rh.get(RenderingHints.KEY_TEXT_ANTIALIASING) ==
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)) {
// In both these cases we want the non-antialiased
// font render context.
frc = aaOffFontRenderContext;
}
layout = getTextLayoutFactory().createTextLayout
(runaci, subCharMap, offset, frc);
textRuns.add(new TextRun(layout, runaci, isChunkStart));
// System.out.println("TextRun: " + start + "->" + end +
// " Start: " + isChunkStart);
Point2D layoutAdvance = layout.getAdvance2D();
// System.out.println("layoutAdv: " + layoutAdvance);
advance.x += (float)layoutAdvance.getX();
advance.y += (float)layoutAdvance.getY();
++endChunk;
if (aci.setIndex(end) == CharacterIterator.DONE) break;
isChunkStart = false;
} while (true);
// System.out.println("Adv: " + advance);
// System.out.println("Chunks: [" + beginChunk + ", " +
// endChunk + "]");
return new TextChunk(beginChunk, endChunk, advance);
}
/**
* Adjusts the position of the text runs within the specified text chunk
* to account for any text anchor properties.
*/
private Point2D adjustChunkOffsets(Point2D location,
List textRuns,
TextChunk chunk) {
TextRun r = (TextRun) textRuns.get(chunk.begin);
int anchorType = r.getAnchorType();
Float length = r.getLength();
Integer lengthAdj = r.getLengthAdjust();
boolean doAdjust = true;
if ((length == null) || length.isNaN())
doAdjust = false;
int numChars = 0;
for (int n=chunk.begin; n<chunk.end; ++n) {
r = (TextRun) textRuns.get(n);
AttributedCharacterIterator aci = r.getACI();
numChars += aci.getEndIndex()-aci.getBeginIndex();
}
if ((lengthAdj ==
GVTAttributedCharacterIterator.TextAttribute.ADJUST_SPACING) &&
(numChars == 1))
doAdjust = false;
float xScale = 1;
float yScale = 1;
r = (TextRun)textRuns.get(chunk.end-1);
TextSpanLayout layout = r.getLayout();
GVTGlyphMetrics lastMetrics =
layout.getGlyphMetrics(layout.getGlyphCount()-1);
Rectangle2D lastBounds = lastMetrics.getBounds2D();
float lastW = (float)(lastBounds.getWidth()+lastBounds.getX());
float lastH = (float)(lastBounds.getHeight());
Point2D visualAdvance;
if (!doAdjust) {
// System.out.println("Adv: " + chunk.advance);
// System.out.println("LastBounds: " + lastBounds);
// System.out.println("LastMetrics.hadv: " + lastMetrics.getHorizontalAdvance());
// System.out.println("LastMetrics.vadv: " + lastMetrics.getVerticalAdvance());
visualAdvance = new Point2D.Float
((float)(chunk.advance.getX() + lastW -
lastMetrics.getHorizontalAdvance()),
(float)(chunk.advance.getY() + lastH -
lastMetrics.getVerticalAdvance()));
} else {
Point2D advance = chunk.advance;
// We have to do this here since textLength needs to be
// handled at the text chunk level. Otherwise tspans get
// messed up.
float delta = 0;
if (layout.isVertical()) {
if (lengthAdj == ADJUST_SPACING) {
yScale = (float)
((length.floatValue()-lastH)/
(advance.getY()-lastMetrics.getVerticalAdvance()));
} else {
double adv = (advance.getY()-
lastMetrics.getVerticalAdvance() + lastH);
yScale = (float)(length.floatValue()/adv);
}
visualAdvance = new Point2D.Float(0, length.floatValue());
} else {
if (lengthAdj == ADJUST_SPACING) {
xScale = (float)
((length.floatValue()-lastW)/
(advance.getX()-lastMetrics.getHorizontalAdvance()));
} else {
double adv = (advance.getX() + lastW -
lastMetrics.getHorizontalAdvance());
xScale = (float)(length.floatValue()/adv);
}
visualAdvance = new Point2D.Float(length.floatValue(), 0);
}
// System.out.println("Adv: " + advance + " Len: " + length +
// " scale: [" + xScale + ", " + yScale + "]");
Point2D.Float adv = new Point2D.Float(0,0);
for (int n=chunk.begin; n<chunk.end; ++n) {
r = (TextRun) textRuns.get(n);
layout = r.getLayout();
layout.setScale(xScale, yScale, lengthAdj==ADJUST_SPACING);
Point2D lAdv = layout.getAdvance2D();
adv.x += (float)lAdv.getX();
adv.y += (float)lAdv.getY();
}
chunk.advance = adv;
}
float dx = 0f;
float dy = 0f;
switch(anchorType){
case TextNode.Anchor.ANCHOR_MIDDLE:
dx = (float) (-visualAdvance.getX()/2d);
dy = (float) (-visualAdvance.getY()/2d);
break;
case TextNode.Anchor.ANCHOR_END:
dx = (float) (-visualAdvance.getX());
dy = (float) (-visualAdvance.getY());
break;
default:
break;
// leave untouched
}
// System.out.println("DX/DY: [" + dx + ", " + dy + "]");
r = (TextRun) textRuns.get(chunk.begin);
layout = r.getLayout();
AttributedCharacterIterator runaci = r.getACI();
runaci.first();
boolean vertical = layout.isVertical();
Float runX = (Float) runaci.getAttribute(XPOS);
Float runY = (Float) runaci.getAttribute(YPOS);
TextPath textPath = (TextPath) runaci.getAttribute(TEXTPATH);
// The point that the next peice of normal text should be
// layed out from, only used for normal text not text on a path.
float absX = (float)location.getX();
float absY = (float)location.getY();
// TextPath Shift used to account for startOffset.
float tpShiftX = 0;
float tpShiftY = 0;
// Of course X and Y override that, but they don't apply for
// text on a path.
if ((runX != null) && (!runX.isNaN())) {
absX = runX.floatValue();
tpShiftX = absX;
}
if ((runY != null) && (!runY.isNaN())) {
absY = runY.floatValue();
tpShiftY = absY;
}
// Factor in text-anchor in writing direction.
// Ignore tpShift in non-writing direction.
if (vertical) {
absY += dy;
tpShiftY += dy;
tpShiftX = 0;
} else {
absX += dx;
tpShiftX += dx;
tpShiftY = 0;
}
// System.out.println("ABS: [" + absX + "," + absY + "," +
// visualAdvance.getX() + "," +
// visualAdvance.getY() + "]");
for (int n=chunk.begin; n<chunk.end; ++n) {
r = (TextRun) textRuns.get(n);
layout = r.getLayout();
runaci = r.getACI();
runaci.first();
textPath = (TextPath) runaci.getAttribute(TEXTPATH);
if (vertical) {
runX = (Float) runaci.getAttribute(XPOS);
if ((runX != null) && (!runX.isNaN())) {
absX = runX.floatValue();
}
} else {
runY = (Float) runaci.getAttribute(YPOS);
if ((runY != null) && (!runY.isNaN())) {
absY = runY.floatValue();
}
}
if (textPath == null) {
layout.setOffset(new Point2D.Float(absX, absY));
Point2D ladv = layout.getAdvance2D();
absX += ladv.getX();
absY += ladv.getY();
} else {
layout.setOffset(new Point2D.Float(tpShiftX, tpShiftY));
Point2D ladv = layout.getAdvance2D();
tpShiftX += (float)ladv.getX();
tpShiftY += (float)ladv.getY();
ladv = layout.getTextPathAdvance();
absX = (float)ladv.getX();
absY = (float)ladv.getY();
}
}
return new Point2D.Float(absX, absY);
}
/**
* Paints decorations of the specified type.
*/
private void paintDecorations(List textRuns,
Graphics2D g2d,
int decorationType) {
Paint prevPaint = null;
Paint prevStrokePaint = null;
Stroke prevStroke = null;
Rectangle2D decorationRect = null;
double yLoc = 0, height = 0;
for (int i = 0; i < textRuns.size(); i++) {
TextRun textRun = (TextRun)textRuns.get(i);
AttributedCharacterIterator runaci = textRun.getACI();
runaci.first();
Composite opacity = (Composite)
runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.OPACITY);
if (opacity != null) {
g2d.setComposite(opacity);
}
Paint paint = null;
Stroke stroke = null;
Paint strokePaint = null;
switch (decorationType) {
case TextSpanLayout.DECORATION_UNDERLINE :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_STROKE_PAINT);
break;
case TextSpanLayout.DECORATION_OVERLINE :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_STROKE_PAINT);
break;
case TextSpanLayout.DECORATION_STRIKETHROUGH :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_STROKE_PAINT);
break;
default:
// should never get here
return;
}
if (textRun.isFirstRunInChunk()) {
Shape s = textRun.getLayout().getDecorationOutline
(decorationType);
Rectangle2D r2d = s.getBounds2D();
yLoc = r2d.getY();
height = r2d.getHeight();
}
if (textRun.isFirstRunInChunk() ||
(paint != prevPaint) ||
(stroke != prevStroke) ||
(strokePaint != prevStrokePaint)) {
// if there is a current decoration, draw it now
if (decorationRect != null) {
if (prevPaint != null) {
// fill the decoration
g2d.setPaint(prevPaint);
g2d.fill(decorationRect);
}
if (prevStroke != null && prevStrokePaint != null) {
// stroke the decoration
g2d.setPaint(prevStrokePaint);
g2d.setStroke(prevStroke);
g2d.draw(decorationRect);
}
decorationRect = null;
}
}
if ((paint != null || strokePaint != null)
&& !textRun.getLayout().isVertical()
&& !textRun.getLayout().isOnATextPath()) {
// this text run should be decorated with the
// specified decoration type
// NOTE: decorations are only supported for plain
// horizontal layouts
Shape decorationShape =
textRun.getLayout().getDecorationOutline(decorationType);
if (decorationRect == null) {
// create a new one
Rectangle2D r2d = decorationShape.getBounds2D();
decorationRect = new Rectangle2D.Double
(r2d.getX(), yLoc, r2d.getWidth(), height);
} else {
// extend the current one
Rectangle2D bounds = decorationShape.getBounds2D();
double minX = Math.min(decorationRect.getX(),
bounds.getX());
double maxX = Math.max(decorationRect.getMaxX(),
bounds.getMaxX());
decorationRect.setRect(minX, yLoc, maxX-minX, height);
}
}
prevPaint = paint;
prevStroke = stroke;
prevStrokePaint = strokePaint;
}
// if there is a decoration rect that hasn't been drawn yet, draw it now
if (decorationRect != null) {
if (prevPaint != null) {
// fill the decoration
g2d.setPaint(prevPaint);
g2d.fill(decorationRect);
}
if (prevStroke != null && prevStrokePaint != null) {
// stroke the decoration
g2d.setPaint(prevStrokePaint);
g2d.setStroke(prevStroke);
g2d.draw(decorationRect);
}
}
}
/**
* Paints the text in each text run. Decorations are not painted here.
*/
private void paintTextRuns(List textRuns,
Graphics2D g2d) {
for (int i = 0; i < textRuns.size(); i++) {
TextRun textRun = (TextRun)textRuns.get(i);
AttributedCharacterIterator runaci = textRun.getACI();
runaci.first();
Composite opacity = (Composite)
runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.OPACITY);
if (opacity != null) {
g2d.setComposite(opacity);
}
textRun.getLayout().draw(g2d);
}
}
/**
* Get a Shape in userspace coords which defines the textnode glyph outlines.
* @param node the TextNode to measure
*/
public Shape getOutline(TextNode node) {
GeneralPath outline = null;
AttributedCharacterIterator aci = node.getAttributedCharacterIterator();
// get the list of text runs
List textRuns = getTextRuns(node, aci);
// for each text run, get its outline and append it to the overall
// outline
for (int i = 0; i < textRuns.size(); ++i) {
TextRun textRun = (TextRun)textRuns.get(i);
TextSpanLayout textRunLayout = textRun.getLayout();
GeneralPath textRunOutline =
new GeneralPath(textRunLayout.getOutline());
if (outline == null) {
outline = textRunOutline;
} else {
outline.setWindingRule(GeneralPath.WIND_NON_ZERO);
outline.append(textRunOutline, false);
}
}
// append any decoration outlines
Shape underline = getDecorationOutline
(textRuns, TextSpanLayout.DECORATION_UNDERLINE);
Shape strikeThrough = getDecorationOutline
(textRuns, TextSpanLayout.DECORATION_STRIKETHROUGH);
Shape overline = getDecorationOutline
(textRuns, TextSpanLayout.DECORATION_OVERLINE);
if (underline != null) {
if (outline == null) {
outline = new GeneralPath(underline);
} else {
outline.setWindingRule(GeneralPath.WIND_NON_ZERO);
outline.append(underline, false);
}
}
if (strikeThrough != null) {
if (outline == null) {
outline = new GeneralPath(strikeThrough);
} else {
outline.setWindingRule(GeneralPath.WIND_NON_ZERO);
outline.append(strikeThrough, false);
}
}
if (overline != null) {
if (outline == null) {
outline = new GeneralPath(overline);
} else {
outline.setWindingRule(GeneralPath.WIND_NON_ZERO);
outline.append(overline, false);
}
}
return outline;
}
/**
* Get a Rectangle2D in userspace coords which encloses the textnode
* glyphs including stroke etc.
*/
public Rectangle2D getBounds2D(TextNode node) {
AttributedCharacterIterator aci = node.getAttributedCharacterIterator();
// get the list of text runs
List textRuns = getTextRuns(node, aci);
Rectangle2D bounds = null;
// for each text run, get its stroke outline and append it to the overall outline
for (int i = 0; i < textRuns.size(); ++i) {
Shape textRunStrokeOutline = null;
TextRun textRun = (TextRun)textRuns.get(i);
AttributedCharacterIterator textRunACI = textRun.getACI();
textRunACI.first();
TextSpanLayout textRunLayout = textRun.getLayout();
if (bounds == null)
bounds = textRunLayout.getBounds2D();
else
bounds = bounds.createUnion(textRunLayout.getBounds2D());
}
// append any stroked decoration outlines
Shape underline = getDecorationStrokeOutline
(textRuns, TextSpanLayout.DECORATION_UNDERLINE);
if (underline != null) {
if (bounds == null)
bounds = underline.getBounds2D();
else
bounds = bounds.createUnion(underline.getBounds2D());
}
Shape strikeThrough = getDecorationStrokeOutline
(textRuns, TextSpanLayout.DECORATION_STRIKETHROUGH);
if (strikeThrough != null) {
if (bounds == null)
bounds = strikeThrough.getBounds2D();
else
bounds = bounds.createUnion(strikeThrough.getBounds2D());
}
Shape overline = getDecorationStrokeOutline
(textRuns, TextSpanLayout.DECORATION_OVERLINE);
if (overline != null) {
if (bounds == null)
bounds = overline.getBounds2D();
else
bounds = bounds.createUnion(overline.getBounds2D());
}
return bounds;
}
/**
* Returns the outline of the specified decoration type.
*
* @param textRuns The list of text runs to get the decoration outline for.
* @param decoratonType Indicates the type of decoration required.
* eg. underline, overline or strikethrough.
*
* @return The decoration outline or null if the text is not decorated.
*/
private Shape getDecorationOutline(List textRuns, int decorationType) {
GeneralPath outline = null;
Paint prevPaint = null;
Paint prevStrokePaint = null;
Stroke prevStroke = null;
Rectangle2D decorationRect = null;
double yLoc = 0, height = 0;
for (int i = 0; i < textRuns.size(); i++) {
TextRun textRun = (TextRun)textRuns.get(i);
AttributedCharacterIterator runaci = textRun.getACI();
runaci.first();
Paint paint = null;
Stroke stroke = null;
Paint strokePaint = null;
switch (decorationType) {
case TextSpanLayout.DECORATION_UNDERLINE :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_STROKE_PAINT);
break;
case TextSpanLayout.DECORATION_OVERLINE :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_STROKE_PAINT);
break;
case TextSpanLayout.DECORATION_STRIKETHROUGH :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_STROKE_PAINT);
break;
default:
// should never get here
return null;
}
if (textRun.isFirstRunInChunk()) {
Shape s = textRun.getLayout().getDecorationOutline
(decorationType);
Rectangle2D r2d = s.getBounds2D();
yLoc = r2d.getY();
height = r2d.getHeight();
}
if (textRun.isFirstRunInChunk() ||
paint != prevPaint ||
stroke != prevStroke ||
strokePaint != prevStrokePaint) {
// if there is a current decoration, added it to the overall
// outline
if (decorationRect != null) {
if (outline == null) {
outline = new GeneralPath(decorationRect);
} else {
outline.append(decorationRect, false);
}
decorationRect = null;
}
}
if ((paint != null || strokePaint != null)
&& !textRun.getLayout().isVertical()
&& !textRun.getLayout().isOnATextPath()) {
// this text run should be decorated with the specified
// decoration type note: decorations are only supported for
// plain horizontal layouts
Shape decorationShape =
textRun.getLayout().getDecorationOutline(decorationType);
if (decorationRect == null) {
// create a new one
Rectangle2D r2d = decorationShape.getBounds2D();
decorationRect = new Rectangle2D.Double
(r2d.getX(), yLoc, r2d.getWidth(), height);
} else {
// extend the current one
Rectangle2D bounds = decorationShape.getBounds2D();
double minX = Math.min(decorationRect.getX(),
bounds.getX());
double maxX = Math.max(decorationRect.getMaxX(),
bounds.getMaxX());
decorationRect.setRect(minX, yLoc, maxX-minX, height);
}
}
prevPaint = paint;
prevStroke = stroke;
prevStrokePaint = strokePaint;
}
// if there is a decoration rect that hasn't been added to the overall outline
if (decorationRect != null) {
if (outline == null) {
outline = new GeneralPath(decorationRect);
} else {
outline.append(decorationRect, false);
}
}
return outline;
}
/**
* Returns the strokeed outline of the specified decoration type.
* If the decoration has no stroke it will return the fill outline
*
* @param textRuns The list of text runs to get the decoration outline for.
* @param decoratonType Indicates the type of decoration required.
* eg. underline, overline or strikethrough.
*
* @return The decoration outline or null if the text is not decorated.
*/
private Shape getDecorationStrokeOutline
(List textRuns, int decorationType) {
GeneralPath outline = null;
Paint prevPaint = null;
Paint prevStrokePaint = null;
Stroke prevStroke = null;
Rectangle2D decorationRect = null;
double yLoc = 0, height = 0;
for (int i = 0; i < textRuns.size(); i++) {
TextRun textRun = (TextRun)textRuns.get(i);
AttributedCharacterIterator runaci = textRun.getACI();
runaci.first();
Paint paint = null;
Stroke stroke = null;
Paint strokePaint = null;
switch (decorationType) {
case TextSpanLayout.DECORATION_UNDERLINE :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_STROKE_PAINT);
break;
case TextSpanLayout.DECORATION_OVERLINE :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.OVERLINE_STROKE_PAINT);
break;
case TextSpanLayout.DECORATION_STRIKETHROUGH :
paint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_PAINT);
stroke = (Stroke) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_STROKE);
strokePaint = (Paint) runaci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.STRIKETHROUGH_STROKE_PAINT);
break;
default:
// should never get here
return null;
}
if (textRun.isFirstRunInChunk()) {
Shape s = textRun.getLayout().getDecorationOutline
(decorationType);
Rectangle2D r2d = s.getBounds2D();
yLoc = r2d.getY();
height = r2d.getHeight();
}
if (textRun.isFirstRunInChunk() ||
paint != prevPaint ||
stroke != prevStroke ||
strokePaint != prevStrokePaint) {
// if there is a current decoration, added it to the overall
// outline
if (decorationRect != null) {
Shape s = null;
if (prevStroke != null &&
prevStrokePaint != null)
s = prevStroke.createStrokedShape(decorationRect);
else if (prevPaint != null)
s = decorationRect;
if (s != null) {
if (outline == null)
outline = new GeneralPath(s);
else
outline.append(s, false);
}
decorationRect = null;
}
}
if ((paint != null || strokePaint != null)
&& !textRun.getLayout().isVertical()
&& !textRun.getLayout().isOnATextPath()) {
// this text run should be decorated with the specified
// decoration type note: decorations are only supported for
// plain horizontal layouts
Shape decorationShape =
textRun.getLayout().getDecorationOutline(decorationType);
if (decorationRect == null) {
// create a new one
Rectangle2D r2d = decorationShape.getBounds2D();
decorationRect = new Rectangle2D.Double
(r2d.getX(), yLoc, r2d.getWidth(), height);
} else {
// extend the current one
Rectangle2D bounds = decorationShape.getBounds2D();
double minX = Math.min(decorationRect.getX(),
bounds.getX());
double maxX = Math.max(decorationRect.getMaxX(),
bounds.getMaxX());
decorationRect.setRect(minX, yLoc, maxX-minX, height);
}
}
prevPaint = paint;
prevStroke = stroke;
prevStrokePaint = strokePaint;
}
// if there is a decoration rect that hasn't been added to the overall
// outline
if (decorationRect != null) {
Shape s = null;
if (prevStroke != null &&
prevStrokePaint != null)
s = prevStroke.createStrokedShape(decorationRect);
else if (prevPaint != null)
s = decorationRect;
if (s != null) {
if (outline == null)
outline = new GeneralPath(s);
else
outline.append(s, false);
}
}
return outline;
}
public Mark getMark(TextNode node, int index, boolean leadingEdge) {
AttributedCharacterIterator aci;
aci = node.getAttributedCharacterIterator();
if ((index < aci.getBeginIndex()) ||
(index > aci.getEndIndex()))
return null;
TextHit textHit = new TextHit(index, leadingEdge);
return new BasicTextPainter.BasicMark(node, textHit);
}
protected Mark hitTest(double x, double y, TextNode node) {
AttributedCharacterIterator aci;
aci = node.getAttributedCharacterIterator();
// get the list of text runs
List textRuns = getTextRuns(node, aci);
// for each text run, see if its been hit
for (int i = 0; i < textRuns.size(); ++i) {
TextRun textRun = (TextRun)textRuns.get(i);
TextSpanLayout layout = textRun.getLayout();
TextHit textHit = layout.hitTestChar((float) x, (float) y);
if (textHit != null && layout.getBounds2D().contains(x,y)) {
return new BasicTextPainter.BasicMark(node, textHit);
}
}
return null;
}
/**
* Selects the first glyph in the text node.
*/
public Mark selectFirst(TextNode node) {
AttributedCharacterIterator aci;
aci = node.getAttributedCharacterIterator();
TextHit textHit = new TextHit(aci.getBeginIndex(), false);
return new BasicTextPainter.BasicMark(node, textHit);
}
/**
* Selects the last glyph in the text node.
*/
public Mark selectLast(TextNode node) {
AttributedCharacterIterator aci;
aci = node.getAttributedCharacterIterator();
TextHit textHit = new TextHit(aci.getEndIndex(), false);
return new BasicTextPainter.BasicMark(node, textHit);
}
/**
* Returns an array of ints representing begin/end index pairs into
* an AttributedCharacterIterator which represents the text
* selection delineated by two Mark instances.
* <em>Note: The Mark instances passed must have been instantiated by
* an instance of this enclosing TextPainter implementation.</em>
*/
public int[] getSelected(Mark startMark,
Mark finishMark) {
if (startMark == null || finishMark == null) {
return null;
}
BasicTextPainter.BasicMark start;
BasicTextPainter.BasicMark finish;
try {
start = (BasicTextPainter.BasicMark) startMark;
finish = (BasicTextPainter.BasicMark) finishMark;
} catch (ClassCastException cce) {
throw new
Error("This Mark was not instantiated by this TextPainter class!");
}
TextNode textNode = start.getTextNode();
if (textNode != finish.getTextNode())
throw new Error("Markers are from different TextNodes!");
AttributedCharacterIterator aci;
aci = textNode.getAttributedCharacterIterator();
int[] result = new int[2];
result[0] = start.getHit().getCharIndex();
result[1] = finish.getHit().getCharIndex();
// get the list of text runs
List textRuns = getTextRuns(textNode, aci);
Iterator trI = textRuns.iterator();
int startGlyphIndex = -1;
int endGlyphIndex = -1;
TextSpanLayout startLayout=null, endLayout=null;
while (trI.hasNext()) {
TextRun tr = (TextRun)trI.next();
TextSpanLayout tsl = tr.getLayout();
if (startGlyphIndex == -1) {
startGlyphIndex = tsl.getGlyphIndex(result[0]);
if (startGlyphIndex != -1)
startLayout = tsl;
}
if (endGlyphIndex == -1) {
endGlyphIndex = tsl.getGlyphIndex(result[1]);
if (endGlyphIndex != -1)
endLayout = tsl;
}
if ((startGlyphIndex != -1) && (endGlyphIndex != -1))
break;
}
if ((startLayout == null) || (endLayout == null))
return null;
int startCharCount = startLayout.getCharacterCount
(startGlyphIndex, startGlyphIndex);
int endCharCount = endLayout.getCharacterCount
(endGlyphIndex, endGlyphIndex);
if (startCharCount > 1) {
if (result[0] > result[1] && startLayout.isLeftToRight()) {
result[0] += startCharCount-1;
} else if (result[1] > result[0] && !startLayout.isLeftToRight()) {
result[0] -= startCharCount-1;
}
}
if (endCharCount > 1) {
if (result[1] > result[0] && endLayout.isLeftToRight()) {
result[1] += endCharCount-1;
} else if (result[0] > result[1] && !endLayout.isLeftToRight()) {
result[1] -= endCharCount-1;
}
}
return result;
}
/**
* Return a Shape, in the coordinate system of the text layout,
* which encloses the text selection delineated by two Mark instances.
* <em>Note: The Mark instances passed must have been instantiated by
* an instance of this enclosing TextPainter implementation.</em>
*/
public Shape getHighlightShape(Mark beginMark, Mark endMark) {
if (beginMark == null || endMark == null) {
return null;
}
BasicTextPainter.BasicMark begin;
BasicTextPainter.BasicMark end;
try {
begin = (BasicTextPainter.BasicMark) beginMark;
end = (BasicTextPainter.BasicMark) endMark;
} catch (ClassCastException cce) {
throw new Error
("This Mark was not instantiated by this TextPainter class!");
}
TextNode textNode = begin.getTextNode();
if (textNode != end.getTextNode())
throw new Error("Markers are from different TextNodes!");
if (textNode == null)
return null;
int beginIndex = begin.getHit().getCharIndex();
int endIndex = end.getHit().getCharIndex();
if (beginIndex > endIndex) {
// Swap them...
BasicTextPainter.BasicMark tmpMark = begin;
begin = end; end = tmpMark;
int tmpIndex = beginIndex;
beginIndex = endIndex; endIndex = tmpIndex;
}
// get the list of text runs
List textRuns = getTextRuns
(textNode, textNode.getAttributedCharacterIterator());
GeneralPath highlightedShape = new GeneralPath();
// for each text run, append any highlight it may contain for
// the current selection
for (int i = 0; i < textRuns.size(); ++i) {
TextRun textRun = (TextRun)textRuns.get(i);
TextSpanLayout layout = textRun.getLayout();
Shape layoutHighlightedShape = layout.getHighlightShape
(beginIndex, endIndex);
// append the highlighted shape of this layout to the
// overall hightlighted shape
if (( layoutHighlightedShape != null) &&
(!layoutHighlightedShape.getBounds().isEmpty())) {
highlightedShape.append(layoutHighlightedShape, false);
}
}
return highlightedShape;
}
// inner classes
class TextChunk {
public int begin;
public int end;
public Point2D advance;
public TextChunk(int begin, int end, Point2D advance) {
this.begin = begin;
this.end = end;
this.advance = new Point2D.Float((float) advance.getX(),
(float) advance.getY());
}
}
/**
* Inner convenience class for associating a TextLayout for
* sub-spans, and the ACI which iterates over that subspan.
*/
public class TextRun {
private AttributedCharacterIterator aci;
private TextSpanLayout layout;
private int anchorType;
private boolean firstRunInChunk;
private Float length;
private Integer lengthAdjust;
public TextRun(TextSpanLayout layout,
AttributedCharacterIterator aci,
boolean firstRunInChunk) {
this.layout = layout;
this.aci = aci;
this.aci.first();
this.firstRunInChunk = firstRunInChunk;
this.anchorType = TextNode.Anchor.ANCHOR_START;
TextNode.Anchor anchor = (TextNode.Anchor) aci.getAttribute
(GVTAttributedCharacterIterator.TextAttribute.ANCHOR_TYPE);
if (anchor != null) {
this.anchorType = anchor.getType();
}
// if writing mode is right to left, then need to reverse the
// text anchor positions
if (aci.getAttribute(WRITING_MODE) == WRITING_MODE_RTL) {
if (anchorType == TextNode.Anchor.ANCHOR_START) {
anchorType = TextNode.Anchor.ANCHOR_END;
} else if (anchorType == TextNode.Anchor.ANCHOR_END) {
anchorType = TextNode.Anchor.ANCHOR_START;
}
// leave middle as is
}
length = (Float) aci.getAttribute
(GVTAttributedCharacterIterator.TextAttribute.BBOX_WIDTH);
lengthAdjust = (Integer) aci.getAttribute
(GVTAttributedCharacterIterator.TextAttribute.LENGTH_ADJUST);
}
public AttributedCharacterIterator getACI() {
return aci;
}
public TextSpanLayout getLayout() {
return layout;
}
public int getAnchorType() {
return anchorType;
}
public Float getLength() {
return length;
}
public Integer getLengthAdjust() {
return lengthAdjust;
}
public boolean isFirstRunInChunk() {
return firstRunInChunk;
}
}
}