/* * 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. */ /** * @author Oleg V. Khaschansky * @version $Revision$ * */ package org.apache.harmony.awt.gl.font; import java.awt.*; import java.awt.font.*; import java.awt.geom.Rectangle2D; import java.awt.geom.GeneralPath; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; // XXX - TODO - bidi not implemented yet //import java.text.Bidi; import java.util.Arrays; import org.apache.harmony.awt.internal.nls.Messages; /** * Date: Apr 25, 2005 * Time: 4:33:18 PM * * This class contains the implementation of the behavior of the * text run segment with constant text attributes and direction. */ public class TextRunSegmentImpl { /** * This class contains basic information required for creation * of the glyph-based text run segment. */ public static class TextSegmentInfo { // XXX - TODO - bidi not implemented yet //Bidi bidi; Font font; FontRenderContext frc; char text[]; int start; int end; int length; int flags = 0; byte level = 0; TextSegmentInfo( byte level, Font font, FontRenderContext frc, char text[], int start, int end ) { this.font = font; this.frc = frc; this.text = text; this.start = start; this.end = end; this.level = level; length = end - start; } } /** * This class represents a simple text segment backed by the glyph vector */ public static class TextRunSegmentCommon extends TextRunSegment { TextSegmentInfo info; private GlyphVector gv; private float advanceIncrements[]; private int char2glyph[]; private GlyphJustificationInfo gjis[]; // Glyph justification info TextRunSegmentCommon(TextSegmentInfo i, TextDecorator.Decoration d) { // XXX - todo - check support bidi i.flags &= ~0x09; // Clear bidi flags if ((i.level & 0x1) != 0) { i.flags |= Font.LAYOUT_RIGHT_TO_LEFT; } info = i; this.decoration = d; LineMetrics lm = i.font.getLineMetrics(i.text, i.start, i.end, i.frc); this.metrics = new BasicMetrics(lm, i.font); if (lm.getNumChars() != i.length) { // XXX todo - This should be handled // awt.41=Font returned unsupported type of line metrics. This case is known, but not supported yet. throw new UnsupportedOperationException( Messages.getString("awt.41")); //$NON-NLS-1$ } } @Override public Object clone() { return new TextRunSegmentCommon(info, decoration); } /** * Creates glyph vector from the managed text if needed * @return glyph vector */ private GlyphVector getGlyphVector() { if (gv==null) { gv = info.font.layoutGlyphVector( info.frc, info.text, info.start, info.end - info.start, // NOTE: This parameter violates // spec, it is count, // not limit as spec states info.flags ); } return gv; } /** * Renders this text run segment * @param g2d - graphics to render to * @param xOffset - X offset from the graphics origin to the * origin of the text layout * @param yOffset - Y offset from the graphics origin to the * origin of the text layout */ @Override void draw(Graphics2D g2d, float xOffset, float yOffset) { if (decoration == null) { g2d.drawGlyphVector(getGlyphVector(), xOffset + x, yOffset + y); } else { TextDecorator.prepareGraphics(this, g2d, xOffset, yOffset); g2d.drawGlyphVector(getGlyphVector(), xOffset + x, yOffset + y); TextDecorator.drawTextDecorations(this, g2d, xOffset, yOffset); TextDecorator.restoreGraphics(decoration, g2d); } } /** * Returns visual bounds of this segment * @return visual bounds */ @Override Rectangle2D getVisualBounds() { if (visualBounds == null) { visualBounds = TextDecorator.extendVisualBounds( this, getGlyphVector().getVisualBounds(), decoration ); visualBounds.setRect( x + visualBounds.getX(), y + visualBounds.getY(), visualBounds.getWidth(), visualBounds.getHeight() ); } return (Rectangle2D) visualBounds.clone(); } /** * Returns logical bounds of this segment * @return logical bounds */ @Override Rectangle2D getLogicalBounds() { if (logicalBounds == null) { logicalBounds = getGlyphVector().getLogicalBounds(); logicalBounds.setRect( x + logicalBounds.getX(), y + logicalBounds.getY(), logicalBounds.getWidth(), logicalBounds.getHeight() ); } return (Rectangle2D) logicalBounds.clone(); } @Override float getAdvance() { return (float) getLogicalBounds().getWidth(); } /** * Attemts to map each character to the corresponding advance increment */ void initAdvanceMapping() { GlyphVector gv = getGlyphVector(); int charIndicies[] = gv.getGlyphCharIndices(0, gv.getNumGlyphs(), null); advanceIncrements = new float[info.length]; for (int i=0; i<charIndicies.length; i++) { advanceIncrements[charIndicies[i]] = gv.getGlyphMetrics(i).getAdvance(); } } /** * Calculates advance delta between two characters * @param start - 1st position * @param end - 2nd position * @return advance increment between specified positions */ @Override float getAdvanceDelta(int start, int end) { // Get coordinates in the segment context start -= info.start; end -= info.start; if (advanceIncrements == null) { initAdvanceMapping(); } if (start < 0) { start = 0; } if (end > info.length) { end = info.length; } float sum = 0; for (int i=start; i<end; i++) { sum += advanceIncrements[i]; } return sum; } /** * Calculates index of the character which advance is equal to * the given. If the given advance is greater then the segment * advance it returns the position after the last character. * @param advance - given advance * @param start - character, from which to start measuring advance * @return character index */ @Override int getCharIndexFromAdvance(float advance, int start) { // XXX - todo - probably, possible to optimize // Add check if the given advance is greater then // the segment advance in the beginning. In this case // we don't need to run through all increments if (advanceIncrements == null) { initAdvanceMapping(); } start -= info.start; if (start < 0) { start = 0; } int i = start; for (; i<info.length; i++) { advance -= advanceIncrements[i]; if (advance < 0) { break; } } return i + info.start; } @Override int getStart() { return info.start; } @Override int getEnd() { return info.end; } @Override int getLength() { return info.length; } /** * Attemts to create mapping of the characters to glyphs in the glyph vector. * @return array where for each character index stored corresponding glyph index */ private int[] getChar2Glyph() { if (char2glyph == null) { GlyphVector gv = getGlyphVector(); char2glyph = new int[info.length]; Arrays.fill(char2glyph, -1); // Fill glyph indicies for first characters corresponding to each glyph int charIndicies[] = gv.getGlyphCharIndices(0, gv.getNumGlyphs(), null); for (int i=0; i<charIndicies.length; i++) { char2glyph[charIndicies[i]] = i; } // If several characters corresponds to one glyph, create mapping for them // Suppose that these characters are going all together int currIndex = 0; for (int i=0; i<char2glyph.length; i++) { if (char2glyph[i] < 0) { char2glyph[i] = currIndex; } else { currIndex = char2glyph[i]; } } } return char2glyph; } /** * Creates black box bounds shape for the specified range * @param start - range sart * @param limit - range end * @return black box bounds shape */ @Override Shape getCharsBlackBoxBounds(int start, int limit) { start -= info.start; limit -= info.start; if (limit > info.length) { limit = info.length; } GeneralPath result = new GeneralPath(); int glyphIndex = 0; for (int i=start; i<limit; i++) { glyphIndex = getChar2Glyph()[i]; result.append(getGlyphVector().getGlyphVisualBounds(glyphIndex), false); } // Shift to the segment's coordinates result.transform(AffineTransform.getTranslateInstance(x, y)); return result; } /** * Calculates position of the character on the screen * @param index - character index * @return X coordinate of the character position */ @Override float getCharPosition(int index) { index -= info.start; if (index > info.length) { index = info.length; } float result = 0; int glyphIndex = getChar2Glyph()[index]; result = (float) getGlyphVector().getGlyphPosition(glyphIndex).getX(); // Shift to the segment's coordinates result += x; return result; } /** * Returns the advance of the individual character * @param index - character index * @return character advance */ @Override float getCharAdvance(int index) { if (advanceIncrements == null) { initAdvanceMapping(); } return advanceIncrements[index - this.getStart()]; } /** * Returns the outline shape * @return outline */ @Override Shape getOutline() { AffineTransform t = AffineTransform.getTranslateInstance(x, y); return t.createTransformedShape( TextDecorator.extendOutline( this, getGlyphVector().getOutline(), decoration ) ); } /** * Checks if the character doesn't contribute to the text advance * @param index - character index * @return true if the character has zero advance */ @Override boolean charHasZeroAdvance(int index) { if (advanceIncrements == null) { initAdvanceMapping(); } return advanceIncrements[index - this.getStart()] == 0; } /** * Creates text hit info from the hit position * @param hitX - X coordinate relative to the origin of the layout * @param hitY - Y coordinate relative to the origin of the layout * @return hit info */ @Override TextHitInfo hitTest(float hitX, float hitY) { hitX -= x; float glyphPositions[] = getGlyphVector().getGlyphPositions(0, info.length+1, null); int glyphIdx; boolean leading = false; for (glyphIdx = 1; glyphIdx <= info.length; glyphIdx++) { if (glyphPositions[(glyphIdx)*2] >= hitX) { float advance = glyphPositions[(glyphIdx)*2] - glyphPositions[(glyphIdx-1)*2]; leading = glyphPositions[(glyphIdx-1)*2] + advance/2 > hitX ? true : false; glyphIdx--; break; } } if (glyphIdx == info.length) { glyphIdx--; } int charIdx = getGlyphVector().getGlyphCharIndex(glyphIdx); return (leading) ^ ((info.level & 0x1) == 0x1)? TextHitInfo.leading(charIdx + info.start) : TextHitInfo.trailing(charIdx + info.start); } /** * Collects GlyphJustificationInfo objects from the glyph vector * @return array of all GlyphJustificationInfo objects */ private GlyphJustificationInfo[] getGlyphJustificationInfos() { if (gjis == null) { GlyphVector gv = getGlyphVector(); int nGlyphs = gv.getNumGlyphs(); int charIndicies[] = gv.getGlyphCharIndices(0, nGlyphs, null); gjis = new GlyphJustificationInfo[nGlyphs]; // Patch: temporary patch, getGlyphJustificationInfo is not implemented float fontSize = info.font.getSize2D(); GlyphJustificationInfo defaultInfo = new GlyphJustificationInfo( 0, // weight false, GlyphJustificationInfo.PRIORITY_NONE, 0, 0, // grow false, GlyphJustificationInfo.PRIORITY_NONE, 0, 0); // shrink GlyphJustificationInfo spaceInfo = new GlyphJustificationInfo( fontSize, // weight true, GlyphJustificationInfo.PRIORITY_WHITESPACE, 0, fontSize, // grow true, GlyphJustificationInfo.PRIORITY_WHITESPACE, 0, fontSize); // shrink //////// // Temporary patch, getGlyphJustificationInfo is not implemented for (int i = 0; i < nGlyphs; i++) { //gjis[i] = getGlyphVector().getGlyphJustificationInfo(i); char c = info.text[charIndicies[i] + info.start]; if (Character.isWhitespace(c)) { gjis[i] = spaceInfo; } else { gjis[i] = defaultInfo; } // End patch } } return gjis; } /** * Collects justification information into JustificationInfo object * @param jInfo - JustificationInfo object */ @Override void updateJustificationInfo(TextRunBreaker.JustificationInfo jInfo) { int lastChar = Math.min(jInfo.lastIdx, info.end) - info.start; boolean haveFirst = info.start <= jInfo.firstIdx; boolean haveLast = info.end >= (jInfo.lastIdx + 1); int prevGlyphIdx = -1; int currGlyphIdx; if (jInfo.grow) { // Check how much we can grow/shrink on current priority level for (int i=0; i<lastChar; i++) { currGlyphIdx = getChar2Glyph()[i]; if (currGlyphIdx == prevGlyphIdx) { // Several chars could be represented by one glyph, // suppose they are contiguous continue; } prevGlyphIdx = currGlyphIdx; GlyphJustificationInfo gji = getGlyphJustificationInfos()[currGlyphIdx]; if (gji.growPriority == jInfo.priority) { jInfo.weight += gji.weight * 2; jInfo.growLimit += gji.growLeftLimit; jInfo.growLimit += gji.growRightLimit; if (gji.growAbsorb) { jInfo.absorbedWeight += gji.weight * 2; } } } } else { for (int i=0; i<lastChar; i++) { currGlyphIdx = getChar2Glyph()[i]; if (currGlyphIdx == prevGlyphIdx) { continue; } prevGlyphIdx = currGlyphIdx; GlyphJustificationInfo gji = getGlyphJustificationInfos()[currGlyphIdx]; if (gji.shrinkPriority == jInfo.priority) { jInfo.weight += gji.weight * 2; jInfo.growLimit -= gji.shrinkLeftLimit; jInfo.growLimit -= gji.shrinkRightLimit; if (gji.shrinkAbsorb) { jInfo.absorbedWeight += gji.weight * 2; } } } } if (haveFirst) { // Don't add padding before first char GlyphJustificationInfo gji = getGlyphJustificationInfos()[getChar2Glyph()[0]]; jInfo.weight -= gji.weight; if (jInfo.grow) { jInfo.growLimit -= gji.growLeftLimit; if (gji.growAbsorb) { jInfo.absorbedWeight -= gji.weight; } } else { jInfo.growLimit += gji.shrinkLeftLimit; if (gji.shrinkAbsorb) { jInfo.absorbedWeight -= gji.weight; } } } if (haveLast) { // Don't add padding after last char GlyphJustificationInfo gji = getGlyphJustificationInfos()[getChar2Glyph()[lastChar]]; jInfo.weight -= gji.weight; if (jInfo.grow) { jInfo.growLimit -= gji.growRightLimit; if (gji.growAbsorb) { jInfo.absorbedWeight -= gji.weight; } } else { jInfo.growLimit += gji.shrinkRightLimit; if (gji.shrinkAbsorb) { jInfo.absorbedWeight -= gji.weight; } } } } /** * Performs justification of the segment. * Updates positions of individual characters. * @param jInfos - justification information, gathered by the previous passes * @return amount of growth or shrink of the segment */ @Override float doJustification(TextRunBreaker.JustificationInfo jInfos[]) { int lastPriority = jInfos[jInfos.length-1] == null ? -1 : jInfos[jInfos.length-1].priority; // Get the highest priority int highestPriority = 0; for (; highestPriority<jInfos.length; highestPriority++) { if (jInfos[highestPriority] != null) { break; } } if (highestPriority == jInfos.length) { return 0; } TextRunBreaker.JustificationInfo firstInfo = jInfos[highestPriority]; TextRunBreaker.JustificationInfo lastInfo = lastPriority > 0 ? jInfos[lastPriority] : null; boolean haveFirst = info.start <= firstInfo.firstIdx; boolean haveLast = info.end >= (firstInfo.lastIdx + 1); // Here we suppose that GLYPHS are ordered LEFT TO RIGHT int firstGlyph = haveFirst ? getChar2Glyph()[firstInfo.firstIdx - info.start] : getChar2Glyph()[0]; int lastGlyph = haveLast ? getChar2Glyph()[firstInfo.lastIdx - info.start] : getChar2Glyph()[info.length - 1]; if (haveLast) { lastGlyph--; } TextRunBreaker.JustificationInfo currInfo; float glyphOffset = 0; float positionIncrement = 0; float sideIncrement = 0; if (haveFirst) { // Don't add padding before first char GlyphJustificationInfo gji = getGlyphJustificationInfos()[firstGlyph]; currInfo = jInfos[gji.growPriority]; if (currInfo != null) { if (currInfo.useLimits) { if (currInfo.absorb) { glyphOffset += gji.weight * currInfo.absorbedGapPerUnit; } else if ( lastInfo != null && lastInfo.priority == currInfo.priority ) { glyphOffset += gji.weight * lastInfo.absorbedGapPerUnit; } glyphOffset += firstInfo.grow ? gji.growRightLimit : -gji.shrinkRightLimit; } else { glyphOffset += gji.weight * currInfo.gapPerUnit; } } firstGlyph++; } if (firstInfo.grow) { for (int i=firstGlyph; i<=lastGlyph; i++) { GlyphJustificationInfo gji = getGlyphJustificationInfos()[i]; currInfo = jInfos[gji.growPriority]; if (currInfo == null) { // We still have to increment glyph position Point2D glyphPos = getGlyphVector().getGlyphPosition(i); glyphPos.setLocation(glyphPos.getX() + glyphOffset, glyphPos.getY()); getGlyphVector().setGlyphPosition(i, glyphPos); continue; } if (currInfo.useLimits) { glyphOffset += gji.growLeftLimit; if (currInfo.absorb) { sideIncrement = gji.weight * currInfo.absorbedGapPerUnit; glyphOffset += sideIncrement; positionIncrement = glyphOffset; glyphOffset += sideIncrement; } else if (lastInfo != null && lastInfo.priority == currInfo.priority) { sideIncrement = gji.weight * lastInfo.absorbedGapPerUnit; glyphOffset += sideIncrement; positionIncrement = glyphOffset; glyphOffset += sideIncrement; } else { positionIncrement = glyphOffset; } glyphOffset += gji.growRightLimit; } else { sideIncrement = gji.weight * currInfo.gapPerUnit; glyphOffset += sideIncrement; positionIncrement = glyphOffset; glyphOffset += sideIncrement; } Point2D glyphPos = getGlyphVector().getGlyphPosition(i); glyphPos.setLocation(glyphPos.getX() + positionIncrement, glyphPos.getY()); getGlyphVector().setGlyphPosition(i, glyphPos); } } else { for (int i=firstGlyph; i<=lastGlyph; i++) { GlyphJustificationInfo gji = getGlyphJustificationInfos()[i]; currInfo = jInfos[gji.shrinkPriority]; if (currInfo == null) { // We still have to increment glyph position Point2D glyphPos = getGlyphVector().getGlyphPosition(i); glyphPos.setLocation(glyphPos.getX() + glyphOffset, glyphPos.getY()); getGlyphVector().setGlyphPosition(i, glyphPos); continue; } if (currInfo.useLimits) { glyphOffset -= gji.shrinkLeftLimit; if (currInfo.absorb) { sideIncrement = gji.weight * currInfo.absorbedGapPerUnit; glyphOffset += sideIncrement; positionIncrement = glyphOffset; glyphOffset += sideIncrement; } else if (lastInfo != null && lastInfo.priority == currInfo.priority) { sideIncrement = gji.weight * lastInfo.absorbedGapPerUnit; glyphOffset += sideIncrement; positionIncrement = glyphOffset; glyphOffset += sideIncrement; } else { positionIncrement = glyphOffset; } glyphOffset -= gji.shrinkRightLimit; } else { sideIncrement = gji.weight * currInfo.gapPerUnit; glyphOffset += sideIncrement; positionIncrement = glyphOffset; glyphOffset += sideIncrement; } Point2D glyphPos = getGlyphVector().getGlyphPosition(i); glyphPos.setLocation(glyphPos.getX() + positionIncrement, glyphPos.getY()); getGlyphVector().setGlyphPosition(i, glyphPos); } } if (haveLast) { // Don't add padding after last char lastGlyph++; GlyphJustificationInfo gji = getGlyphJustificationInfos()[lastGlyph]; currInfo = jInfos[gji.growPriority]; if (currInfo != null) { if (currInfo.useLimits) { glyphOffset += firstInfo.grow ? gji.growLeftLimit : -gji.shrinkLeftLimit; if (currInfo.absorb) { glyphOffset += gji.weight * currInfo.absorbedGapPerUnit; } else if (lastInfo != null && lastInfo.priority == currInfo.priority) { glyphOffset += gji.weight * lastInfo.absorbedGapPerUnit; } } else { glyphOffset += gji.weight * currInfo.gapPerUnit; } } // Ajust positions of all glyphs after last glyph for (int i=lastGlyph; i<getGlyphVector().getNumGlyphs()+1; i++) { Point2D glyphPos = getGlyphVector().getGlyphPosition(i); glyphPos.setLocation(glyphPos.getX() + glyphOffset, glyphPos.getY()); getGlyphVector().setGlyphPosition(i, glyphPos); } } else { // Update position after last glyph in glyph vector - // to get correct advance for it Point2D glyphPos = getGlyphVector().getGlyphPosition(lastGlyph+1); glyphPos.setLocation(glyphPos.getX() + glyphOffset, glyphPos.getY()); getGlyphVector().setGlyphPosition(lastGlyph+1, glyphPos); } gjis = null; // We don't need justification infos any more // Also we have to reset cached bounds and metrics this.visualBounds = null; this.logicalBounds = null; return glyphOffset; // How much our segment grown or shrunk } } public static class TextRunSegmentGraphic extends TextRunSegment { GraphicAttribute ga; int start; int length; float fullAdvance; TextRunSegmentGraphic(GraphicAttribute attr, int len, int start) { this.start = start; length = len; ga = attr; metrics = new BasicMetrics(ga); fullAdvance = ga.getAdvance() * length; } @Override public Object clone() { return new TextRunSegmentGraphic(ga, length, start); } // Renders this text run segment @Override void draw(Graphics2D g2d, float xOffset, float yOffset) { if (decoration != null) { TextDecorator.prepareGraphics(this, g2d, xOffset, yOffset); } float xPos = x + xOffset; float yPos = y + yOffset; for (int i=0; i < length; i++) { ga.draw(g2d, xPos, yPos); xPos += ga.getAdvance(); } if (decoration != null) { TextDecorator.drawTextDecorations(this, g2d, xOffset, yOffset); TextDecorator.restoreGraphics(decoration, g2d); } } // Returns visual bounds of this segment @Override Rectangle2D getVisualBounds() { if (visualBounds == null) { Rectangle2D bounds = ga.getBounds(); // First and last chars can be out of logical bounds, so we calculate // (bounds.getWidth() - ga.getAdvance()) which is exactly the difference bounds.setRect( bounds.getMinX() + x, bounds.getMinY() + y, bounds.getWidth() - ga.getAdvance() + getAdvance(), bounds.getHeight() ); visualBounds = TextDecorator.extendVisualBounds(this, bounds, decoration); } return (Rectangle2D) visualBounds.clone(); } @Override Rectangle2D getLogicalBounds() { if (logicalBounds == null) { logicalBounds = new Rectangle2D.Float( x, y - metrics.ascent, getAdvance(), metrics.ascent + metrics.descent ); } return (Rectangle2D) logicalBounds.clone(); } @Override float getAdvance() { return fullAdvance; } @Override float getAdvanceDelta(int start, int end) { return ga.getAdvance() * (end - start); } @Override int getCharIndexFromAdvance(float advance, int start) { start -= this.start; if (start < 0) { start = 0; } int charOffset = (int) (advance/ga.getAdvance()); if (charOffset + start > length) { return length + this.start; } return charOffset + start + this.start; } @Override int getStart() { return start; } @Override int getEnd() { return start + length; } @Override int getLength() { return length; } @Override Shape getCharsBlackBoxBounds(int start, int limit) { start -= this.start; limit -= this.start; if (limit > length) { limit = length; } Rectangle2D charBounds = ga.getBounds(); charBounds.setRect( charBounds.getX() + ga.getAdvance() * start + x, charBounds.getY() + y, charBounds.getWidth() + ga.getAdvance() * (limit - start), charBounds.getHeight() ); return charBounds; } @Override float getCharPosition(int index) { index -= start; if (index > length) { index = length; } return ga.getAdvance() * index + x; } @Override float getCharAdvance(int index) { return ga.getAdvance(); } @Override Shape getOutline() { AffineTransform t = AffineTransform.getTranslateInstance(x, y); return t.createTransformedShape( TextDecorator.extendOutline(this, getVisualBounds(), decoration) ); } @Override boolean charHasZeroAdvance(int index) { return false; } @Override TextHitInfo hitTest(float hitX, float hitY) { hitX -= x; float tmp = hitX / ga.getAdvance(); int hitIndex = Math.round(tmp); if (tmp > hitIndex) { return TextHitInfo.leading(hitIndex + this.start); } return TextHitInfo.trailing(hitIndex + this.start); } @Override void updateJustificationInfo(TextRunBreaker.JustificationInfo jInfo) { // Do nothing } @Override float doJustification(TextRunBreaker.JustificationInfo jInfos[]) { // Do nothing return 0; } } }