| /* |
| |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| */ |
| |
| package org.apache.batik.gvt.renderer; |
| |
| 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 org.apache.batik.gvt.TextNode; |
| import org.apache.batik.gvt.TextPainter; |
| import org.apache.batik.gvt.font.DefaultFontFamilyResolver; |
| 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.GVTLineMetrics; |
| 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.TextPaintInfo; |
| 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 <code>TextNode</code>. |
| * <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="mailto:bill.haneman@ireland.sun.com">Bill Haneman</a> |
| * @version $Id$ |
| */ |
| public class StrokingTextPainter extends BasicTextPainter { |
| |
| public static final |
| AttributedCharacterIterator.Attribute PAINT_INFO = |
| GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO; |
| |
| 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_ID |
| = GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_ID; |
| |
| public static final |
| AttributedCharacterIterator.Attribute GVT_FONT |
| = GVTAttributedCharacterIterator.TextAttribute.GVT_FONT; |
| |
| public static final |
| AttributedCharacterIterator.Attribute GVT_FONTS |
| = GVTAttributedCharacterIterator.TextAttribute.GVT_FONTS; |
| |
| 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; |
| |
| |
| public static final AttributedCharacterIterator.Attribute WRITING_MODE |
| = GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE; |
| |
| public static final Integer WRITING_MODE_TTB |
| = GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE_TTB; |
| |
| public 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_ID); |
| 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(); |
| if (aci == null) |
| return; |
| |
| 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); |
| } |
| |
| protected 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); |
| textRuns = computeTextRuns(node, aci, chunkACIs); |
| |
| // 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 node.getTextRuns(); |
| } |
| |
| public List computeTextRuns(TextNode node, |
| AttributedCharacterIterator aci, |
| AttributedCharacterIterator [] chunkACIs) { |
| 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 |
| (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 |
| 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); |
| |
| return textRuns; |
| } |
| |
| /** |
| * Returns an array of ACIs, one for each text chunk within the given |
| * text node. |
| */ |
| protected 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_ID); |
| |
| 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 aci The aci to be modified should already be split into |
| * text chunks. |
| * |
| * @return The new modified aci. |
| */ |
| protected AttributedCharacterIterator createModifiedACIForFontMatching |
| (AttributedCharacterIterator aci) { |
| |
| aci.first(); |
| AttributedString as = null; |
| int asOff = 0; |
| int begin = aci.getBeginIndex(); |
| boolean moreChunks = true; |
| int start, end = aci.getRunStart(TEXT_COMPOUND_ID); |
| while (moreChunks) { |
| start = end; |
| end = aci.getRunLimit(TEXT_COMPOUND_ID); |
| int aciLength = end-start; |
| |
| List fonts; |
| fonts = (List)aci.getAttribute(GVT_FONTS); |
| |
| |
| float fontSize = 12; |
| Float fsFloat = (Float)aci.getAttribute(TextAttribute.SIZE); |
| if (fsFloat != null) |
| fontSize = fsFloat.floatValue(); |
| |
| // if could not resolve at least one of the fontFamilies |
| // then use the default font |
| if (fonts.size() == 0) { |
| // create a list of fonts of the correct size |
| fonts.add(getFontFamilyResolver().getDefault().deriveFont(fontSize, aci)); |
| } |
| |
| // 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 < fonts.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); |
| |
| GVTFont font = (GVTFont)fonts.get(i); |
| if (defaultFont == null) |
| defaultFont = font; |
| |
| while (currentIndex < end) { |
| int displayUpToIndex = font.canDisplayUpTo |
| (aci, currentIndex, end); |
| |
| Object altGlyphElement; |
| altGlyphElement = aci.getAttribute(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 = getFontFamilyResolver().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) == AttributedCharacterIterator.DONE) { |
| moreChunks = false; |
| } |
| start = end; |
| } |
| if (as != null) |
| return as.getIterator(); |
| |
| // Didn't do anything return original ACI |
| return aci; |
| } |
| |
| protected FontFamilyResolver getFontFamilyResolver() { |
| return DefaultFontFamilyResolver.SINGLETON; |
| } |
| |
| protected 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]; |
| System.arraycopy( charMap, start - begin, subCharMap, 0, subCharMap.length ); |
| |
| 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. |
| */ |
| protected 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); |
| GVTLineMetrics lastLineMetrics = layout.getLineMetrics(); |
| Rectangle2D lastBounds = lastMetrics.getBounds2D(); |
| float halfLeading = (lastMetrics.getVerticalAdvance()- |
| (lastLineMetrics.getAscent() + |
| lastLineMetrics.getDescent()))/2; |
| float lastW = (float)(lastBounds.getWidth() + lastBounds.getX()); |
| float lastH = (float)(halfLeading + lastLineMetrics.getAscent() + |
| (lastBounds.getHeight() + lastBounds.getY())); |
| Point2D visualAdvance; |
| |
| if (!doAdjust) { |
| // System.err.println("Anchor: " + anchorType); |
| // System.err.println("Advance: " + chunk.advance); |
| // System.err.println("LastBounds: " + lastBounds); |
| // System.err.println("LastMetrics.hadv: " + |
| // lastMetrics.getHorizontalAdvance()); |
| // System.err.println("LastMetrics.vadv: " + |
| // lastMetrics.getVerticalAdvance()); |
| |
| visualAdvance = new Point2D.Float |
| ((float)(chunk.advance.getX() + lastW - |
| lastMetrics.getHorizontalAdvance()), |
| (float)(chunk.advance.getY() - lastMetrics.getVerticalAdvance() + |
| lastH)); |
| } 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. |
| 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. |
| */ |
| protected void paintDecorations(List textRuns, |
| Graphics2D g2d, |
| int decorationType) { |
| Paint prevPaint = null; |
| Paint prevStrokePaint = null; |
| Stroke prevStroke = null; |
| boolean prevVisible = true; |
| 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; |
| boolean visible = true; |
| TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO); |
| if (tpi != null) { |
| visible = tpi.visible; |
| if (tpi.composite != null) { |
| g2d.setComposite(tpi.composite); |
| } |
| switch (decorationType) { |
| case TextSpanLayout.DECORATION_UNDERLINE : |
| paint = tpi.underlinePaint; |
| stroke = tpi.underlineStroke; |
| strokePaint = tpi.underlineStrokePaint; |
| break; |
| case TextSpanLayout.DECORATION_OVERLINE : |
| paint = tpi.overlinePaint; |
| stroke = tpi.overlineStroke; |
| strokePaint = tpi.overlineStrokePaint; |
| break; |
| case TextSpanLayout.DECORATION_STRIKETHROUGH : |
| paint = tpi.strikethroughPaint; |
| stroke = tpi.strikethroughStroke; |
| strokePaint = tpi.strikethroughStrokePaint; |
| 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) || |
| (visible != prevVisible)) { |
| // if there is a current visible decoration, draw it now |
| if (prevVisible && (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; |
| prevVisible = visible; |
| } |
| |
| // if there is a decoration rect that hasn't been drawn yet and |
| // the text paint info says the test is visible, draw it now. |
| if (prevVisible && (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. |
| */ |
| protected 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(); |
| |
| TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO); |
| if ((tpi != null) && (tpi.composite != null)) { |
| g2d.setComposite(tpi.composite); |
| } |
| 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; |
| aci = node.getAttributedCharacterIterator(); |
| if (aci == null) |
| return null; |
| |
| // 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; |
| aci = node.getAttributedCharacterIterator(); |
| if (aci == null) |
| return null; |
| |
| // 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) { |
| TextRun textRun = (TextRun)textRuns.get(i); |
| TextSpanLayout textRunLayout = textRun.getLayout(); |
| Rectangle2D runBounds = textRunLayout.getBounds2D(); |
| if (runBounds != null) { |
| if (bounds == null) |
| bounds = runBounds; |
| else |
| //bounds = bounds.createUnion(runBounds); |
| bounds.add( runBounds ); |
| } |
| } |
| |
| |
| // 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()); |
| bounds.add( underline.getBounds2D() ); |
| } |
| |
| Shape strikeThrough = getDecorationStrokeOutline |
| (textRuns, TextSpanLayout.DECORATION_STRIKETHROUGH); |
| if (strikeThrough != null) { |
| if (bounds == null) |
| bounds = strikeThrough.getBounds2D(); |
| else |
| //bounds = bounds.createUnion(strikeThrough.getBounds2D()); |
| bounds.add( strikeThrough.getBounds2D() ); |
| } |
| |
| Shape overline = getDecorationStrokeOutline |
| (textRuns, TextSpanLayout.DECORATION_OVERLINE); |
| if (overline != null) { |
| if (bounds == null) |
| bounds = overline.getBounds2D(); |
| else |
| //bounds = bounds.createUnion(overline.getBounds2D()); |
| bounds.add( 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 decorationType Indicates the type of decoration required. |
| * eg. underline, overline or strikethrough. |
| * |
| * @return The decoration outline or null if the text is not decorated. |
| */ |
| protected 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; |
| TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO); |
| if (tpi != null) { |
| switch (decorationType) { |
| case TextSpanLayout.DECORATION_UNDERLINE : |
| paint = tpi.underlinePaint; |
| stroke = tpi.underlineStroke; |
| strokePaint = tpi.underlineStrokePaint; |
| break; |
| case TextSpanLayout.DECORATION_OVERLINE : |
| paint = tpi.overlinePaint; |
| stroke = tpi.overlineStroke; |
| strokePaint = tpi.overlineStrokePaint; |
| break; |
| case TextSpanLayout.DECORATION_STRIKETHROUGH : |
| paint = tpi.strikethroughPaint; |
| stroke = tpi.strikethroughStroke; |
| strokePaint = tpi.strikethroughStrokePaint; |
| 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 stroked 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 decorationType Indicates the type of decoration required. |
| * eg. underline, overline or strikethrough. |
| * |
| * @return The decoration outline or null if the text is not decorated. |
| */ |
| protected 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; |
| TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO); |
| if (tpi != null) { |
| switch (decorationType) { |
| case TextSpanLayout.DECORATION_UNDERLINE : |
| paint = tpi.underlinePaint; |
| stroke = tpi.underlineStroke; |
| strokePaint = tpi.underlineStrokePaint; |
| break; |
| case TextSpanLayout.DECORATION_OVERLINE : |
| paint = tpi.overlinePaint; |
| stroke = tpi.overlineStroke; |
| strokePaint = tpi.overlineStrokePaint; |
| break; |
| case TextSpanLayout.DECORATION_STRIKETHROUGH : |
| paint = tpi.strikethroughPaint; |
| stroke = tpi.strikethroughStroke; |
| strokePaint = tpi.strikethroughStrokePaint; |
| 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 (aci == null) |
| return null; |
| |
| 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(); |
| if (aci == null) |
| return null; |
| |
| // get the list of text runs |
| List textRuns = getTextRuns(node, aci); |
| if (textRuns != null) { |
| // 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); |
| Rectangle2D bounds = layout.getBounds2D(); |
| if ((textHit != null) && |
| (bounds != null) && bounds.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(); |
| if (aci == null) |
| return null; |
| |
| 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(); |
| if (aci == null) |
| return null; |
| |
| TextHit textHit = new TextHit(aci.getEndIndex()-1, 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 == null) |
| return null; |
| if (textNode != finish.getTextNode()) |
| throw new Error("Markers are from different TextNodes!"); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) |
| return null; |
| |
| 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 == null) |
| return null; |
| if (textNode != end.getTextNode()) |
| throw new Error("Markers are from different TextNodes!"); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == 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, aci); |
| |
| 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 { |
| |
| protected AttributedCharacterIterator aci; |
| protected TextSpanLayout layout; |
| protected int anchorType; |
| protected boolean firstRunInChunk; |
| protected Float length; |
| protected 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; |
| } |
| |
| } |
| } |