blob: 106469258ea470b01ba9aaa7ca41dcf535513f7f [file] [log] [blame]
/*
* 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.
*/
/* $Id$ */
package org.apache.fop.svg.font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphJustificationInfo;
import java.awt.font.GlyphMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Arrays;
import java.util.List;
import org.apache.batik.gvt.font.GVTFont;
import org.apache.batik.gvt.font.GVTGlyphMetrics;
import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.font.GVTLineMetrics;
import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontMetrics;
import org.apache.fop.fonts.GlyphMapping;
import org.apache.fop.fonts.TextFragment;
import org.apache.fop.traits.MinOptMax;
public class FOPGVTGlyphVector implements GVTGlyphVector {
protected final TextFragment text;
protected final FOPGVTFont font;
private final int fontSize;
private final FontMetrics fontMetrics;
private final FontRenderContext frc;
protected int[] glyphs;
protected List associations;
protected int[][] gposAdjustments;
protected float[] positions;
protected Rectangle2D[] boundingBoxes;
protected GeneralPath outline;
protected AffineTransform[] glyphTransforms;
protected boolean[] glyphVisibilities;
protected Rectangle2D logicalBounds;
FOPGVTGlyphVector(FOPGVTFont font, final CharacterIterator iter, FontRenderContext frc) {
this.text = new SVGTextFragment(iter);
this.font = font;
Font f = font.getFont();
this.fontSize = f.getFontSize();
this.fontMetrics = f.getFontMetrics();
this.frc = frc;
}
public void performDefaultLayout() {
Font f = font.getFont();
MinOptMax letterSpaceIPD = MinOptMax.ZERO;
MinOptMax[] letterSpaceAdjustments = new MinOptMax[text.getEndIndex()];
boolean retainControls = false;
GlyphMapping mapping = GlyphMapping.doGlyphMapping(text, text.getBeginIndex(), text.getEndIndex(),
f, letterSpaceIPD, letterSpaceAdjustments, '\0', '\0',
false, text.getBidiLevel(), true, true, retainControls);
CharacterIterator glyphAsCharIter =
mapping.mapping != null ? new StringCharacterIterator(mapping.mapping) : text.getIterator();
this.glyphs = buildGlyphs(f, glyphAsCharIter);
this.associations = mapping.associations;
this.gposAdjustments = mapping.gposAdjustments;
if (text.getBeginIndex() > 0) {
int arrlen = text.getEndIndex() - text.getBeginIndex();
MinOptMax[] letterSpaceAdjustmentsNew = new MinOptMax[arrlen];
System.arraycopy(letterSpaceAdjustments, text.getBeginIndex(), letterSpaceAdjustmentsNew,
0, arrlen);
letterSpaceAdjustments = letterSpaceAdjustmentsNew;
}
this.positions = buildGlyphPositions(glyphAsCharIter, mapping.gposAdjustments, letterSpaceAdjustments);
this.glyphVisibilities = new boolean[this.glyphs.length];
Arrays.fill(glyphVisibilities, true);
this.glyphTransforms = new AffineTransform[this.glyphs.length];
}
private static class SVGTextFragment implements TextFragment {
private final CharacterIterator charIter;
private String script;
private String language;
private int level = -1;
SVGTextFragment(CharacterIterator charIter) {
this.charIter = charIter;
if (charIter instanceof AttributedCharacterIterator) {
AttributedCharacterIterator aci = (AttributedCharacterIterator) charIter;
aci.first();
this.script = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.SCRIPT);
this.language = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.LANGUAGE);
Integer level = (Integer) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.BIDI_LEVEL);
if (level != null) {
this.level = level;
}
}
}
public CharacterIterator getIterator() {
return charIter;
}
public int getBeginIndex() {
return charIter.getBeginIndex();
}
public int getEndIndex() {
return charIter.getEndIndex();
}
// TODO - [GA] the following appears to be broken because it ignores
// sttart and end index arguments
public CharSequence subSequence(int startIndex, int endIndex) {
StringBuilder sb = new StringBuilder();
for (char c = charIter.first(); c != CharacterIterator.DONE; c = charIter.next()) {
sb.append(c);
}
return sb.toString();
}
public String getScript() {
if (script != null) {
return script;
} else {
return "auto";
}
}
public String getLanguage() {
if (language != null) {
return language;
} else {
return "none";
}
}
public int getBidiLevel() {
return level;
}
public char charAt(int index) {
return charIter.setIndex(index - charIter.getBeginIndex());
}
}
private int[] buildGlyphs(Font font, final CharacterIterator glyphAsCharIter) {
int[] glyphs = new int[glyphAsCharIter.getEndIndex() - glyphAsCharIter.getBeginIndex()];
int index = 0;
for (char c = glyphAsCharIter.first(); c != CharacterIterator.DONE; c = glyphAsCharIter.next()) {
glyphs[index] = font.mapChar(c);
index++;
}
return glyphs;
}
private static final int[] PA_ZERO = new int[4];
/**
* Build glyph position array.
* @param glyphAsCharIter iterator for mapped glyphs as char codes (not glyph codes)
* @param dp optionally null glyph position adjustments array
* @param lsa optionally null letter space adjustments array
* @return array of floats that denote [X,Y] position pairs for each glyph including
* including an implied subsequent glyph; i.e., returned array contains one more pair
* than the numbers of glyphs, where the position denoted by this last pair represents
* the position after the last glyph has incurred advancement
*/
private float[] buildGlyphPositions(final CharacterIterator glyphAsCharIter, int[][] dp, MinOptMax[] lsa) {
int numGlyphs = glyphAsCharIter.getEndIndex() - glyphAsCharIter.getBeginIndex();
float[] positions = new float[2 * (numGlyphs + 1)];
float xc = 0f;
float yc = 0f;
if (dp != null) {
for (int i = 0; i < numGlyphs + 1; ++i) {
int[] pa = ((i >= dp.length) || (dp[i] == null)) ? PA_ZERO : dp[i];
float xo = xc + ((float) pa[0]) / 1000f;
float yo = yc - ((float) pa[1]) / 1000f;
float xa = getGlyphWidth(i) + ((float) pa[2]) / 1000f;
float ya = ((float) pa[3]) / 1000f;
int k = 2 * i;
positions[k + 0] = xo;
positions[k + 1] = yo;
xc += xa;
yc += ya;
}
} else if (lsa != null) {
for (int i = 0; i < numGlyphs + 1; ++i) {
MinOptMax sa = (((i + 1) >= lsa.length) || (lsa[i + 1] == null)) ? MinOptMax.ZERO : lsa[i + 1];
float xo = xc;
float yo = yc;
float xa = getGlyphWidth(i) + sa.getOpt() / 1000f;
float ya = 0;
int k = 2 * i;
positions[k + 0] = xo;
positions[k + 1] = yo;
xc += xa;
yc += ya;
}
}
return positions;
}
private float getGlyphWidth(int index) {
if (index < glyphs.length) {
return fontMetrics.getWidth(glyphs[index], fontSize) / 1000000f;
} else {
return 0f;
}
}
public GVTFont getFont() {
return font;
}
public FontRenderContext getFontRenderContext() {
return frc;
}
public void setGlyphCode(int glyphIndex, int glyphCode) {
glyphs[glyphIndex] = glyphCode;
}
public int getGlyphCode(int glyphIndex) {
return glyphs[glyphIndex];
}
public int[] getGlyphCodes(int beginGlyphIndex, int numEntries,
int[] codeReturn) {
if (codeReturn == null) {
codeReturn = new int[numEntries];
}
System.arraycopy(glyphs, beginGlyphIndex, codeReturn, 0, numEntries);
return codeReturn;
}
public GlyphJustificationInfo getGlyphJustificationInfo(int glyphIndex) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException();
}
public Shape getGlyphLogicalBounds(int glyphIndex) {
GVTGlyphMetrics metrics = getGlyphMetrics(glyphIndex);
Point2D pos = getGlyphPosition(glyphIndex);
GVTLineMetrics fontMetrics = font.getLineMetrics(0);
Rectangle2D bounds = new Rectangle2D.Float(0, -fontMetrics.getDescent(), metrics.getHorizontalAdvance(),
fontMetrics.getAscent() + fontMetrics.getDescent());
AffineTransform t = AffineTransform.getTranslateInstance(pos.getX(), pos.getY());
AffineTransform transf = getGlyphTransform(glyphIndex);
if (transf != null) {
t.concatenate(transf);
}
t.scale(1, -1); // Translate from glyph coordinate system to user
return t.createTransformedShape(bounds);
}
public GVTGlyphMetrics getGlyphMetrics(int glyphIndex) {
Rectangle2D bbox = getBoundingBoxes()[glyphIndex];
return new GVTGlyphMetrics(positions[2 * (glyphIndex + 1)] - positions[2 * glyphIndex],
(fontMetrics.getAscender(fontSize) - fontMetrics.getDescender(fontSize)) / 1000000f,
bbox, GlyphMetrics.STANDARD);
}
public Shape getGlyphOutline(int glyphIndex) {
Shape glyphBox = getBoundingBoxes()[glyphIndex];
AffineTransform tr = AffineTransform.getTranslateInstance(positions[glyphIndex * 2],
positions[glyphIndex * 2 + 1]);
AffineTransform glyphTransform = getGlyphTransform(glyphIndex);
if (glyphTransform != null) {
tr.concatenate(glyphTransform);
}
return tr.createTransformedShape(glyphBox);
}
public Rectangle2D getGlyphCellBounds(int glyphIndex) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException();
}
public int[][] getGlyphPositionAdjustments() {
return gposAdjustments;
}
public Point2D getGlyphPosition(int glyphIndex) {
int positionIndex = glyphIndex * 2;
return new Point2D.Float(positions[positionIndex], positions[positionIndex + 1]);
}
public float[] getGlyphPositions(int beginGlyphIndex, int numEntries, float[] positionReturn) {
if (positionReturn == null) {
positionReturn = new float[numEntries * 2];
}
System.arraycopy(positions, beginGlyphIndex * 2, positionReturn, 0, numEntries * 2);
return positionReturn;
}
public AffineTransform getGlyphTransform(int glyphIndex) {
return glyphTransforms[glyphIndex];
}
public Shape getGlyphVisualBounds(int glyphIndex) {
Rectangle2D bbox = getBoundingBoxes()[glyphIndex];
Point2D pos = getGlyphPosition(glyphIndex);
AffineTransform t = AffineTransform.getTranslateInstance(pos.getX(), pos.getY());
AffineTransform transf = getGlyphTransform(glyphIndex);
if (transf != null) {
t.concatenate(transf);
}
return t.createTransformedShape(bbox);
}
public Rectangle2D getLogicalBounds() {
if (logicalBounds == null) {
GeneralPath logicalBoundsPath = new GeneralPath();
for (int i = 0; i < getNumGlyphs(); i++) {
Shape glyphLogicalBounds = getGlyphLogicalBounds(i);
logicalBoundsPath.append(glyphLogicalBounds, false);
}
logicalBounds = logicalBoundsPath.getBounds2D();
}
return logicalBounds;
}
public int getNumGlyphs() {
return glyphs.length;
}
public Shape getOutline() {
if (outline == null) {
outline = new GeneralPath();
for (int i = 0; i < glyphs.length; i++) {
outline.append(getGlyphOutline(i), false);
}
}
return outline;
}
public Shape getOutline(float x, float y) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException();
}
public Rectangle2D getGeometricBounds() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException();
}
public Rectangle2D getBounds2D(AttributedCharacterIterator aci) {
return getOutline().getBounds2D();
}
public void setGlyphPosition(int glyphIndex, Point2D newPos) {
int idx = glyphIndex * 2;
positions[idx] = (float) newPos.getX();
positions[idx + 1] = (float) newPos.getY();
}
public void setGlyphTransform(int glyphIndex, AffineTransform newTX) {
glyphTransforms[glyphIndex] = newTX;
}
public void setGlyphVisible(int glyphIndex, boolean visible) {
glyphVisibilities[glyphIndex] = visible;
}
public boolean isGlyphVisible(int glyphIndex) {
return glyphVisibilities[glyphIndex];
}
public int getCharacterCount(int startGlyphIndex, int endGlyphIndex) {
// TODO Not that simple if complex scripts are involved
return endGlyphIndex - startGlyphIndex + 1;
}
public boolean isReversed() {
return false;
}
public void maybeReverse(boolean mirror) {
}
public void draw(Graphics2D graphics2d, AttributedCharacterIterator aci) {
// NOP
}
private Rectangle2D[] getBoundingBoxes() {
if (boundingBoxes == null) {
buildBoundingBoxes();
}
return boundingBoxes;
}
private void buildBoundingBoxes() {
boundingBoxes = new Rectangle2D[glyphs.length];
for (int i = 0; i < glyphs.length; i++) {
Rectangle bbox = fontMetrics.getBoundingBox(glyphs[i], fontSize);
boundingBoxes[i] = new Rectangle2D.Float(bbox.x / 1000000f, -(bbox.y + bbox.height) / 1000000f,
bbox.width / 1000000f, bbox.height / 1000000f);
}
}
}