mirror of
https://github.com/openjdk/jdk.git
synced 2026-02-05 07:58:40 +00:00
1951 lines
70 KiB
Java
1951 lines
70 KiB
Java
/*
|
|
* Copyright (c) 1998, 2017, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package sun.font;
|
|
|
|
import java.awt.Font;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Point;
|
|
import java.awt.Rectangle;
|
|
import static java.awt.RenderingHints.*;
|
|
import java.awt.Shape;
|
|
import java.awt.font.FontRenderContext;
|
|
import java.awt.font.GlyphMetrics;
|
|
import java.awt.font.GlyphJustificationInfo;
|
|
import java.awt.font.GlyphVector;
|
|
import java.awt.font.LineMetrics;
|
|
import java.awt.font.TextAttribute;
|
|
import java.awt.geom.AffineTransform;
|
|
import java.awt.geom.GeneralPath;
|
|
import java.awt.geom.NoninvertibleTransformException;
|
|
import java.awt.geom.PathIterator;
|
|
import java.awt.geom.Point2D;
|
|
import java.awt.geom.Rectangle2D;
|
|
import java.lang.ref.SoftReference;
|
|
import java.text.CharacterIterator;
|
|
|
|
import sun.awt.SunHints;
|
|
import sun.java2d.loops.FontInfo;
|
|
|
|
/**
|
|
* Standard implementation of GlyphVector used by Font, GlyphList, and
|
|
* SunGraphics2D.
|
|
*
|
|
* The main issues involve the semantics of the various transforms
|
|
* (font, glyph, device) and their effect on rendering and metrics.
|
|
*
|
|
* Very, very unfortunately, the translation component of the font
|
|
* transform affects where the text gets rendered. It offsets the
|
|
* rendering origin. None of the other metrics of the glyphvector
|
|
* are affected, making them inconsistent with the rendering behavior.
|
|
* I think the translation component of the font would be better
|
|
* interpreted as the translation component of a per-glyph transform,
|
|
* but I don't know if this is possible to change.
|
|
*
|
|
* After the font transform is applied, the glyph transform is
|
|
* applied. This makes glyph transforms relative to font transforms,
|
|
* if the font transform changes, the glyph transform will have the
|
|
* same (relative) effect on the outline of the glyph. The outline
|
|
* and logical bounds are passed through the glyph transform before
|
|
* being returned. The glyph metrics ignore the glyph transform, but
|
|
* provide the outline bounds and the advance vector of the glyph (the
|
|
* latter will be rotated if the font is rotated). The default layout
|
|
* places each glyph at the end of the advance vector of the previous
|
|
* glyph, and since the glyph transform translates the advance vector,
|
|
* this means a glyph transform affects the positions of all
|
|
* subsequent glyphs if defaultLayout is called after setting a glyph
|
|
* transform. In the glyph info array, the bounds are the outline
|
|
* bounds including the glyph transform, and the positions are as
|
|
* computed, and the advances are the deltas between the positions.
|
|
*
|
|
* (There's a bug in the logical bounds of a rotated glyph for
|
|
* composite fonts, it's not to spec (in 1.4.0, 1.4.1, 1.4.2). The
|
|
* problem is that the rotated composite doesn't handle the multiple
|
|
* ascents and descents properly in both x and y. You end up with
|
|
* a rotated advance vector but an unrotated ascent and descent.)
|
|
*
|
|
* Finally, the whole thing is transformed by the device transform to
|
|
* position it on the page.
|
|
*
|
|
* Another bug: The glyph outline seems to ignore fractional point
|
|
* size information, but the images (and advances) don't ignore it.
|
|
*
|
|
* Small fonts drawn at large magnification have odd advances when
|
|
* fractional metrics is off-- that's because the advances depend on
|
|
* the frc. When the frc is scaled appropriately, the advances are
|
|
* fine. FM or a large frc (high numbers) make the advances right.
|
|
*
|
|
* The buffer aa flag doesn't affect rendering, the glyph vector
|
|
* renders as AA if aa is set in its frc, and as non-aa if aa is not
|
|
* set in its frc.
|
|
*
|
|
* font rotation, baseline, vertical etc.
|
|
*
|
|
* Font rotation and baseline Line metrics should be measured along a
|
|
* unit vector pi/4 cc from the baseline vector. For 'horizontal'
|
|
* fonts the baseline vector is the x vector passed through the font
|
|
* transform (ignoring translation), for 'vertical' it is the y
|
|
* vector. This definition makes ascent, descent, etc independent of
|
|
* shear, so shearing can be used to simulate italic. This means no
|
|
* fonts have 'negative ascents' or 'zero ascents' etc.
|
|
*
|
|
* Having a coordinate system with orthogonal axes where one is
|
|
* parallel to the baseline means we could use rectangles and interpret
|
|
* them in terms of this coordinate system. Unfortunately there
|
|
* is support for rotated fonts in the jdk already so maintaining
|
|
* the semantics of existing code (getlogical bounds, etc) might
|
|
* be difficult.
|
|
*
|
|
* A font transform transforms both the baseline and all the glyphs
|
|
* in the font, so it does not rotate the glyph w.r.t the baseline.
|
|
* If you do want to rotate individual glyphs, you need to apply a
|
|
* glyph transform. If performDefaultLayout is called after this,
|
|
* the transformed glyph advances will affect the glyph positions.
|
|
*
|
|
* useful additions
|
|
* - select vertical metrics - glyphs are rotated pi/4 cc and vertical
|
|
* metrics are used to align them to the baseline.
|
|
* - define baseline for font (glyph rotation not linked to baseline)
|
|
* - define extra space (delta between each glyph along baseline)
|
|
* - define offset (delta from 'true' baseline, impacts ascent and
|
|
* descent as these are still computed from true basline and pinned
|
|
* to zero, used in superscript).
|
|
*/
|
|
public class StandardGlyphVector extends GlyphVector {
|
|
private Font font;
|
|
private FontRenderContext frc;
|
|
private int[] glyphs; // always
|
|
private int[] userGlyphs; // used to return glyphs to the client.
|
|
private float[] positions; // only if not default advances
|
|
private int[] charIndices; // only if interesting
|
|
private int flags; // indicates whether positions, charIndices is interesting
|
|
|
|
private static final int UNINITIALIZED_FLAGS = -1;
|
|
|
|
// transforms information
|
|
private GlyphTransformInfo gti; // information about per-glyph transforms
|
|
|
|
// !!! can we get rid of any of this extra stuff?
|
|
private AffineTransform ftx; // font transform without translation
|
|
private AffineTransform dtx; // device transform used for strike calculations, no translation
|
|
private AffineTransform invdtx; // inverse of dtx or null if dtx is identity
|
|
private AffineTransform frctx; // font render context transform, wish we could just share it
|
|
private Font2D font2D; // basic strike-independent stuff
|
|
private SoftReference<GlyphStrike> fsref; // font strike reference for glyphs with no per-glyph transform
|
|
|
|
/////////////////////////////
|
|
// Constructors and Factory methods
|
|
/////////////////////////////
|
|
|
|
public StandardGlyphVector(Font font, String str, FontRenderContext frc) {
|
|
init(font, str.toCharArray(), 0, str.length(), frc, UNINITIALIZED_FLAGS);
|
|
}
|
|
|
|
public StandardGlyphVector(Font font, char[] text, FontRenderContext frc) {
|
|
init(font, text, 0, text.length, frc, UNINITIALIZED_FLAGS);
|
|
}
|
|
|
|
public StandardGlyphVector(Font font, char[] text, int start, int count,
|
|
FontRenderContext frc) {
|
|
init(font, text, start, count, frc, UNINITIALIZED_FLAGS);
|
|
}
|
|
|
|
private float getTracking(Font font) {
|
|
if (font.hasLayoutAttributes()) {
|
|
AttributeValues values = ((AttributeMap)font.getAttributes()).getValues();
|
|
return values.getTracking();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// used by GlyphLayout to construct a glyphvector
|
|
public StandardGlyphVector(Font font, FontRenderContext frc, int[] glyphs, float[] positions,
|
|
int[] indices, int flags) {
|
|
initGlyphVector(font, frc, glyphs, positions, indices, flags);
|
|
|
|
// this code should go into layout
|
|
float track = getTracking(font);
|
|
if (track != 0) {
|
|
track *= font.getSize2D();
|
|
Point2D.Float trackPt = new Point2D.Float(track, 0); // advance delta
|
|
if (font.isTransformed()) {
|
|
AffineTransform at = font.getTransform();
|
|
at.deltaTransform(trackPt, trackPt);
|
|
}
|
|
|
|
// how do we know its a base glyph
|
|
// for now, it is if the natural advance of the glyph is non-zero
|
|
Font2D f2d = FontUtilities.getFont2D(font);
|
|
FontStrike strike = f2d.getStrike(font, frc);
|
|
|
|
float[] deltas = { trackPt.x, trackPt.y };
|
|
for (int j = 0; j < deltas.length; ++j) {
|
|
float inc = deltas[j];
|
|
if (inc != 0) {
|
|
float delta = 0;
|
|
for (int i = j, n = 0; n < glyphs.length; i += 2) {
|
|
if (strike.getGlyphAdvance(glyphs[n++]) != 0) { // might be an inadequate test
|
|
positions[i] += delta;
|
|
delta += inc;
|
|
}
|
|
}
|
|
positions[positions.length-2+j] += delta;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void initGlyphVector(Font font, FontRenderContext frc, int[] glyphs, float[] positions,
|
|
int[] indices, int flags) {
|
|
this.font = font;
|
|
this.frc = frc;
|
|
this.glyphs = glyphs;
|
|
this.userGlyphs = glyphs; // no need to check
|
|
this.positions = positions;
|
|
this.charIndices = indices;
|
|
this.flags = flags;
|
|
|
|
initFontData();
|
|
}
|
|
|
|
public StandardGlyphVector(Font font, CharacterIterator iter, FontRenderContext frc) {
|
|
int offset = iter.getBeginIndex();
|
|
char[] text = new char [iter.getEndIndex() - offset];
|
|
for(char c = iter.first();
|
|
c != CharacterIterator.DONE;
|
|
c = iter.next()) {
|
|
text[iter.getIndex() - offset] = c;
|
|
}
|
|
init(font, text, 0, text.length, frc, UNINITIALIZED_FLAGS);
|
|
}
|
|
|
|
public StandardGlyphVector(Font font, int[] glyphs, FontRenderContext frc) {
|
|
// !!! find callers of this
|
|
// should be able to fully init from raw data, e.g. charmap, flags too.
|
|
this.font = font;
|
|
this.frc = frc;
|
|
this.flags = UNINITIALIZED_FLAGS;
|
|
|
|
initFontData();
|
|
this.userGlyphs = glyphs;
|
|
this.glyphs = getValidatedGlyphs(this.userGlyphs);
|
|
}
|
|
|
|
/* This is called from the rendering loop. FontInfo is supplied
|
|
* because a GV caches a strike and glyph images suitable for its FRC.
|
|
* LCD text isn't currently supported on all surfaces, in which case
|
|
* standard AA must be used. This is most likely to occur when LCD text
|
|
* is requested and the surface is some non-standard type or hardward
|
|
* surface for which there are no accelerated loops.
|
|
* We can detect this as being AA=="ON" in the FontInfo and AA!="ON"
|
|
* and AA!="GASP" in the FRC - since this only occurs for LCD text we don't
|
|
* need to check any more precisely what value is in the FRC.
|
|
*/
|
|
public static StandardGlyphVector getStandardGV(GlyphVector gv,
|
|
FontInfo info) {
|
|
if (info.aaHint == SunHints.INTVAL_TEXT_ANTIALIAS_ON) {
|
|
Object aaHint = gv.getFontRenderContext().getAntiAliasingHint();
|
|
if (aaHint != VALUE_TEXT_ANTIALIAS_ON &&
|
|
aaHint != VALUE_TEXT_ANTIALIAS_GASP) {
|
|
/* We need to create a new GV with AA==ON for rendering */
|
|
FontRenderContext frc = gv.getFontRenderContext();
|
|
frc = new FontRenderContext(frc.getTransform(),
|
|
VALUE_TEXT_ANTIALIAS_ON,
|
|
frc.getFractionalMetricsHint());
|
|
return new StandardGlyphVector(gv, frc);
|
|
}
|
|
}
|
|
if (gv instanceof StandardGlyphVector) {
|
|
return (StandardGlyphVector)gv;
|
|
}
|
|
return new StandardGlyphVector(gv, gv.getFontRenderContext());
|
|
}
|
|
|
|
/////////////////////////////
|
|
// GlyphVector API
|
|
/////////////////////////////
|
|
|
|
public Font getFont() {
|
|
return this.font;
|
|
}
|
|
|
|
public FontRenderContext getFontRenderContext() {
|
|
return this.frc;
|
|
}
|
|
|
|
public void performDefaultLayout() {
|
|
positions = null;
|
|
if (getTracking(font) == 0) {
|
|
clearFlags(FLAG_HAS_POSITION_ADJUSTMENTS);
|
|
}
|
|
}
|
|
|
|
public int getNumGlyphs() {
|
|
return glyphs.length;
|
|
}
|
|
|
|
public int getGlyphCode(int glyphIndex) {
|
|
return userGlyphs[glyphIndex];
|
|
}
|
|
|
|
public int[] getGlyphCodes(int start, int count, int[] result) {
|
|
if (count < 0) {
|
|
throw new IllegalArgumentException("count = " + count);
|
|
}
|
|
if (start < 0) {
|
|
throw new IndexOutOfBoundsException("start = " + start);
|
|
}
|
|
if (start > glyphs.length - count) { // watch out for overflow if index + count overlarge
|
|
throw new IndexOutOfBoundsException("start + count = " + (start + count));
|
|
}
|
|
|
|
if (result == null) {
|
|
result = new int[count];
|
|
}
|
|
|
|
// if arraycopy were faster, we wouldn't code this
|
|
for (int i = 0; i < count; ++i) {
|
|
result[i] = userGlyphs[i + start];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public int getGlyphCharIndex(int ix) {
|
|
if (ix < 0 && ix >= glyphs.length) {
|
|
throw new IndexOutOfBoundsException("" + ix);
|
|
}
|
|
if (charIndices == null) {
|
|
if ((getLayoutFlags() & FLAG_RUN_RTL) != 0) {
|
|
return glyphs.length - 1 - ix;
|
|
}
|
|
return ix;
|
|
}
|
|
return charIndices[ix];
|
|
}
|
|
|
|
public int[] getGlyphCharIndices(int start, int count, int[] result) {
|
|
if (start < 0 || count < 0 || (count > glyphs.length - start)) {
|
|
throw new IndexOutOfBoundsException("" + start + ", " + count);
|
|
}
|
|
if (result == null) {
|
|
result = new int[count];
|
|
}
|
|
if (charIndices == null) {
|
|
if ((getLayoutFlags() & FLAG_RUN_RTL) != 0) {
|
|
for (int i = 0, n = glyphs.length - 1 - start;
|
|
i < count; ++i, --n) {
|
|
result[i] = n;
|
|
}
|
|
} else {
|
|
for (int i = 0, n = start; i < count; ++i, ++n) {
|
|
result[i] = n;
|
|
}
|
|
}
|
|
} else {
|
|
for (int i = 0; i < count; ++i) {
|
|
result[i] = charIndices[i + start];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// !!! not cached, assume TextLayout will cache if necessary
|
|
// !!! reexamine for per-glyph-transforms
|
|
// !!! revisit for text-on-a-path, vertical
|
|
public Rectangle2D getLogicalBounds() {
|
|
setFRCTX();
|
|
initPositions();
|
|
|
|
LineMetrics lm = font.getLineMetrics("", frc);
|
|
|
|
float minX, minY, maxX, maxY;
|
|
// horiz only for now...
|
|
minX = 0;
|
|
minY = -lm.getAscent();
|
|
maxX = 0;
|
|
maxY = lm.getDescent() + lm.getLeading();
|
|
if (glyphs.length > 0) {
|
|
maxX = positions[positions.length - 2];
|
|
}
|
|
|
|
return new Rectangle2D.Float(minX, minY, maxX - minX, maxY - minY);
|
|
}
|
|
|
|
// !!! not cached, assume TextLayout will cache if necessary
|
|
public Rectangle2D getVisualBounds() {
|
|
Rectangle2D result = null;
|
|
for (int i = 0; i < glyphs.length; ++i) {
|
|
Rectangle2D glyphVB = getGlyphVisualBounds(i).getBounds2D();
|
|
if (!glyphVB.isEmpty()) {
|
|
if (result == null) {
|
|
result = glyphVB;
|
|
} else {
|
|
Rectangle2D.union(result, glyphVB, result);
|
|
}
|
|
}
|
|
}
|
|
if (result == null) {
|
|
result = new Rectangle2D.Float(0, 0, 0, 0);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// !!! not cached, assume TextLayout will cache if necessary
|
|
// !!! fontStrike needs a method for this
|
|
public Rectangle getPixelBounds(FontRenderContext renderFRC, float x, float y) {
|
|
return getGlyphsPixelBounds(renderFRC, x, y, 0, glyphs.length);
|
|
}
|
|
|
|
public Shape getOutline() {
|
|
return getGlyphsOutline(0, glyphs.length, 0, 0);
|
|
}
|
|
|
|
public Shape getOutline(float x, float y) {
|
|
return getGlyphsOutline(0, glyphs.length, x, y);
|
|
}
|
|
|
|
// relative to gv origin
|
|
public Shape getGlyphOutline(int ix) {
|
|
return getGlyphsOutline(ix, 1, 0, 0);
|
|
}
|
|
|
|
// relative to gv origin offset by x, y
|
|
public Shape getGlyphOutline(int ix, float x, float y) {
|
|
return getGlyphsOutline(ix, 1, x, y);
|
|
}
|
|
|
|
public Point2D getGlyphPosition(int ix) {
|
|
initPositions();
|
|
|
|
ix *= 2;
|
|
return new Point2D.Float(positions[ix], positions[ix + 1]);
|
|
}
|
|
|
|
public void setGlyphPosition(int ix, Point2D pos) {
|
|
if (ix < 0 || ix > glyphs.length) {
|
|
throw new IndexOutOfBoundsException("ix = " + ix);
|
|
}
|
|
|
|
initPositions();
|
|
|
|
int ix2 = ix << 1;
|
|
positions[ix2] = (float)pos.getX();
|
|
positions[ix2 + 1] = (float)pos.getY();
|
|
|
|
if (ix < glyphs.length) {
|
|
clearCaches(ix);
|
|
}
|
|
addFlags(FLAG_HAS_POSITION_ADJUSTMENTS);
|
|
}
|
|
|
|
public AffineTransform getGlyphTransform(int ix) {
|
|
if (ix < 0 || ix >= glyphs.length) {
|
|
throw new IndexOutOfBoundsException("ix = " + ix);
|
|
}
|
|
if (gti != null) {
|
|
return gti.getGlyphTransform(ix);
|
|
}
|
|
return null; // spec'd as returning null
|
|
}
|
|
|
|
public void setGlyphTransform(int ix, AffineTransform newTX) {
|
|
if (ix < 0 || ix >= glyphs.length) {
|
|
throw new IndexOutOfBoundsException("ix = " + ix);
|
|
}
|
|
|
|
if (gti == null) {
|
|
if (newTX == null || newTX.isIdentity()) {
|
|
return;
|
|
}
|
|
gti = new GlyphTransformInfo(this);
|
|
}
|
|
gti.setGlyphTransform(ix, newTX); // sets flags
|
|
if (gti.transformCount() == 0) {
|
|
gti = null;
|
|
}
|
|
}
|
|
|
|
public int getLayoutFlags() {
|
|
if (flags == UNINITIALIZED_FLAGS) {
|
|
flags = 0;
|
|
|
|
if (charIndices != null && glyphs.length > 1) {
|
|
boolean ltr = true;
|
|
boolean rtl = true;
|
|
|
|
int rtlix = charIndices.length; // rtl index
|
|
for (int i = 0; i < charIndices.length && (ltr || rtl); ++i) {
|
|
int cx = charIndices[i];
|
|
|
|
ltr = ltr && (cx == i);
|
|
rtl = rtl && (cx == --rtlix);
|
|
}
|
|
|
|
if (rtl) flags |= FLAG_RUN_RTL;
|
|
if (!rtl && !ltr) flags |= FLAG_COMPLEX_GLYPHS;
|
|
}
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
public float[] getGlyphPositions(int start, int count, float[] result) {
|
|
if (count < 0) {
|
|
throw new IllegalArgumentException("count = " + count);
|
|
}
|
|
if (start < 0) {
|
|
throw new IndexOutOfBoundsException("start = " + start);
|
|
}
|
|
if (start > glyphs.length + 1 - count) { // watch for overflow
|
|
throw new IndexOutOfBoundsException("start + count = " + (start + count));
|
|
}
|
|
|
|
return internalGetGlyphPositions(start, count, 0, result);
|
|
}
|
|
|
|
public Shape getGlyphLogicalBounds(int ix) {
|
|
if (ix < 0 || ix >= glyphs.length) {
|
|
throw new IndexOutOfBoundsException("ix = " + ix);
|
|
}
|
|
|
|
Shape[] lbcache;
|
|
if (lbcacheRef == null || (lbcache = lbcacheRef.get()) == null) {
|
|
lbcache = new Shape[glyphs.length];
|
|
lbcacheRef = new SoftReference<>(lbcache);
|
|
}
|
|
|
|
Shape result = lbcache[ix];
|
|
if (result == null) {
|
|
setFRCTX();
|
|
initPositions();
|
|
|
|
// !!! ought to return a rectangle2d for simple cases, though the following works for all
|
|
|
|
// get the position, the tx offset, and the x,y advance and x,y adl. The
|
|
// shape is the box formed by adv (width) and adl (height) offset by
|
|
// the position plus the tx offset minus the ascent.
|
|
|
|
ADL adl = new ADL();
|
|
GlyphStrike gs = getGlyphStrike(ix);
|
|
gs.getADL(adl);
|
|
|
|
Point2D.Float adv = gs.strike.getGlyphMetrics(glyphs[ix]);
|
|
|
|
float wx = adv.x;
|
|
float wy = adv.y;
|
|
float hx = adl.descentX + adl.leadingX + adl.ascentX;
|
|
float hy = adl.descentY + adl.leadingY + adl.ascentY;
|
|
float x = positions[ix*2] + gs.dx - adl.ascentX;
|
|
float y = positions[ix*2+1] + gs.dy - adl.ascentY;
|
|
|
|
GeneralPath gp = new GeneralPath();
|
|
gp.moveTo(x, y);
|
|
gp.lineTo(x + wx, y + wy);
|
|
gp.lineTo(x + wx + hx, y + wy + hy);
|
|
gp.lineTo(x + hx, y + hy);
|
|
gp.closePath();
|
|
|
|
result = new DelegatingShape(gp);
|
|
lbcache[ix] = result;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
private SoftReference<Shape[]> lbcacheRef;
|
|
|
|
public Shape getGlyphVisualBounds(int ix) {
|
|
if (ix < 0 || ix >= glyphs.length) {
|
|
throw new IndexOutOfBoundsException("ix = " + ix);
|
|
}
|
|
|
|
Shape[] vbcache;
|
|
if (vbcacheRef == null || (vbcache = vbcacheRef.get()) == null) {
|
|
vbcache = new Shape[glyphs.length];
|
|
vbcacheRef = new SoftReference<>(vbcache);
|
|
}
|
|
|
|
Shape result = vbcache[ix];
|
|
if (result == null) {
|
|
result = new DelegatingShape(getGlyphOutlineBounds(ix));
|
|
vbcache[ix] = result;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
private SoftReference<Shape[]> vbcacheRef;
|
|
|
|
public Rectangle getGlyphPixelBounds(int index, FontRenderContext renderFRC, float x, float y) {
|
|
return getGlyphsPixelBounds(renderFRC, x, y, index, 1);
|
|
}
|
|
|
|
public GlyphMetrics getGlyphMetrics(int ix) {
|
|
if (ix < 0 || ix >= glyphs.length) {
|
|
throw new IndexOutOfBoundsException("ix = " + ix);
|
|
}
|
|
|
|
Rectangle2D vb = getGlyphVisualBounds(ix).getBounds2D();
|
|
Point2D pt = getGlyphPosition(ix);
|
|
vb.setRect(vb.getMinX() - pt.getX(),
|
|
vb.getMinY() - pt.getY(),
|
|
vb.getWidth(),
|
|
vb.getHeight());
|
|
Point2D.Float adv =
|
|
getGlyphStrike(ix).strike.getGlyphMetrics(glyphs[ix]);
|
|
GlyphMetrics gm = new GlyphMetrics(true, adv.x, adv.y,
|
|
vb,
|
|
GlyphMetrics.STANDARD);
|
|
return gm;
|
|
}
|
|
|
|
public GlyphJustificationInfo getGlyphJustificationInfo(int ix) {
|
|
if (ix < 0 || ix >= glyphs.length) {
|
|
throw new IndexOutOfBoundsException("ix = " + ix);
|
|
}
|
|
|
|
// currently we don't have enough information to do this right. should
|
|
// get info from the font and use real OT/GX justification. Right now
|
|
// sun/font/ExtendedTextSourceLabel assigns one of three infos
|
|
// based on whether the char is kanji, space, or other.
|
|
|
|
return null;
|
|
}
|
|
|
|
public boolean equals(GlyphVector rhs) {
|
|
if (this == rhs) {
|
|
return true;
|
|
}
|
|
if (rhs == null) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
StandardGlyphVector other = (StandardGlyphVector)rhs;
|
|
|
|
if (glyphs.length != other.glyphs.length) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < glyphs.length; ++i) {
|
|
if (glyphs[i] != other.glyphs[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!font.equals(other.font)) {
|
|
return false;
|
|
}
|
|
|
|
if (!frc.equals(other.frc)) {
|
|
return false;
|
|
}
|
|
|
|
if ((other.positions == null) != (positions == null)) {
|
|
if (positions == null) {
|
|
initPositions();
|
|
} else {
|
|
other.initPositions();
|
|
}
|
|
}
|
|
|
|
if (positions != null) {
|
|
for (int i = 0; i < positions.length; ++i) {
|
|
if (positions[i] != other.positions[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gti == null) {
|
|
return other.gti == null;
|
|
} else {
|
|
return gti.equals(other.gti);
|
|
}
|
|
}
|
|
catch (ClassCastException e) {
|
|
// assume they are different simply by virtue of the class difference
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* As a concrete subclass of Object that implements equality, this must
|
|
* implement hashCode.
|
|
*/
|
|
public int hashCode() {
|
|
return font.hashCode() ^ glyphs.length;
|
|
}
|
|
|
|
/**
|
|
* Since we implement equality comparisons for GlyphVector, we implement
|
|
* the inherited Object.equals(Object) as well. GlyphVector should do
|
|
* this, and define two glyphvectors as not equal if the classes differ.
|
|
*/
|
|
public boolean equals(Object rhs) {
|
|
try {
|
|
return equals((GlyphVector)rhs);
|
|
}
|
|
catch (ClassCastException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sometimes I wish java had covariant return types...
|
|
*/
|
|
public StandardGlyphVector copy() {
|
|
return (StandardGlyphVector)clone();
|
|
}
|
|
|
|
/**
|
|
* As a concrete subclass of GlyphVector, this must implement clone.
|
|
*/
|
|
public Object clone() {
|
|
// positions, gti are mutable so we have to clone them
|
|
// font2d can be shared
|
|
// fsref is a cache and can be shared
|
|
try {
|
|
StandardGlyphVector result = (StandardGlyphVector)super.clone();
|
|
|
|
result.clearCaches();
|
|
|
|
if (positions != null) {
|
|
result.positions = positions.clone();
|
|
}
|
|
|
|
if (gti != null) {
|
|
result.gti = new GlyphTransformInfo(result, gti);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (CloneNotSupportedException e) {
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
//////////////////////
|
|
// StandardGlyphVector new public methods
|
|
/////////////////////
|
|
|
|
/*
|
|
* Set a multiple glyph positions at one time. GlyphVector only
|
|
* provides API to set a single glyph at a time.
|
|
*/
|
|
public void setGlyphPositions(float[] srcPositions, int srcStart,
|
|
int start, int count) {
|
|
if (count < 0) {
|
|
throw new IllegalArgumentException("count = " + count);
|
|
}
|
|
|
|
initPositions();
|
|
for (int i = start * 2, e = i + count * 2, p = srcStart; i < e; ++i, ++p) {
|
|
positions[i] = srcPositions[p];
|
|
}
|
|
|
|
clearCaches();
|
|
addFlags(FLAG_HAS_POSITION_ADJUSTMENTS);
|
|
}
|
|
|
|
/**
|
|
* Set all the glyph positions, including the 'after last glyph' position.
|
|
* The srcPositions array must be of length (numGlyphs + 1) * 2.
|
|
*/
|
|
public void setGlyphPositions(float[] srcPositions) {
|
|
int requiredLength = glyphs.length * 2 + 2;
|
|
if (srcPositions.length != requiredLength) {
|
|
throw new IllegalArgumentException("srcPositions.length != " + requiredLength);
|
|
}
|
|
|
|
positions = srcPositions.clone();
|
|
|
|
clearCaches();
|
|
addFlags(FLAG_HAS_POSITION_ADJUSTMENTS);
|
|
}
|
|
|
|
/**
|
|
* This is a convenience overload that gets all the glyph positions, which
|
|
* is what you usually want to do if you're getting more than one.
|
|
* !!! should I bother taking result parameter?
|
|
*/
|
|
public float[] getGlyphPositions(float[] result) {
|
|
return internalGetGlyphPositions(0, glyphs.length + 1, 0, result);
|
|
}
|
|
|
|
/**
|
|
* Get transform information for the requested range of glyphs.
|
|
* If no glyphs have a transform, return null.
|
|
* If a glyph has no transform (or is the identity transform) its entry in the result array will be null.
|
|
* If the passed-in result is null an array will be allocated for the caller.
|
|
* Each transform instance in the result array will unique, and independent of the GlyphVector's transform.
|
|
*/
|
|
public AffineTransform[] getGlyphTransforms(int start, int count, AffineTransform[] result) {
|
|
if (start < 0 || count < 0 || start + count > glyphs.length) {
|
|
throw new IllegalArgumentException("start: " + start + " count: " + count);
|
|
}
|
|
|
|
if (gti == null) {
|
|
return null;
|
|
}
|
|
|
|
if (result == null) {
|
|
result = new AffineTransform[count];
|
|
}
|
|
|
|
for (int i = 0; i < count; ++i, ++start) {
|
|
result[i] = gti.getGlyphTransform(start);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Convenience overload for getGlyphTransforms(int, int, AffineTransform[], int);
|
|
*/
|
|
public AffineTransform[] getGlyphTransforms() {
|
|
return getGlyphTransforms(0, glyphs.length, null);
|
|
}
|
|
|
|
/**
|
|
* Set a number of glyph transforms.
|
|
* Original transforms are unchanged. The array may contain nulls, and also may
|
|
* contain multiple references to the same transform instance.
|
|
*/
|
|
public void setGlyphTransforms(AffineTransform[] srcTransforms, int srcStart, int start, int count) {
|
|
for (int i = start, e = start + count; i < e; ++i) {
|
|
setGlyphTransform(i, srcTransforms[srcStart + i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience overload of setGlyphTransforms(AffineTransform[], int, int, int).
|
|
*/
|
|
public void setGlyphTransforms(AffineTransform[] srcTransforms) {
|
|
setGlyphTransforms(srcTransforms, 0, 0, glyphs.length);
|
|
}
|
|
|
|
/**
|
|
* For each glyph return posx, posy, advx, advy, visx, visy, visw, vish.
|
|
*/
|
|
public float[] getGlyphInfo() {
|
|
setFRCTX();
|
|
initPositions();
|
|
float[] result = new float[glyphs.length * 8];
|
|
for (int i = 0, n = 0; i < glyphs.length; ++i, n += 8) {
|
|
float x = positions[i*2];
|
|
float y = positions[i*2+1];
|
|
result[n] = x;
|
|
result[n+1] = y;
|
|
|
|
int glyphID = glyphs[i];
|
|
GlyphStrike s = getGlyphStrike(i);
|
|
Point2D.Float adv = s.strike.getGlyphMetrics(glyphID);
|
|
result[n+2] = adv.x;
|
|
result[n+3] = adv.y;
|
|
|
|
Rectangle2D vb = getGlyphVisualBounds(i).getBounds2D();
|
|
result[n+4] = (float)(vb.getMinX());
|
|
result[n+5] = (float)(vb.getMinY());
|
|
result[n+6] = (float)(vb.getWidth());
|
|
result[n+7] = (float)(vb.getHeight());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* !!! not used currently, but might be by getPixelbounds?
|
|
*/
|
|
public void pixellate(FontRenderContext renderFRC, Point2D loc, Point pxResult) {
|
|
if (renderFRC == null) {
|
|
renderFRC = frc;
|
|
}
|
|
|
|
// it is a total pain that you have to copy the transform.
|
|
|
|
AffineTransform at = renderFRC.getTransform();
|
|
at.transform(loc, loc);
|
|
pxResult.x = (int)loc.getX(); // but must not behave oddly around zero
|
|
pxResult.y = (int)loc.getY();
|
|
loc.setLocation(pxResult.x, pxResult.y);
|
|
try {
|
|
at.inverseTransform(loc, loc);
|
|
}
|
|
catch (NoninvertibleTransformException e) {
|
|
throw new IllegalArgumentException("must be able to invert frc transform");
|
|
}
|
|
}
|
|
|
|
//////////////////////
|
|
// StandardGlyphVector package private methods
|
|
/////////////////////
|
|
|
|
// used by glyphlist to determine if it needs to allocate/size positions array
|
|
// gti always uses positions because the gtx might have translation. We also
|
|
// need positions if the rendering dtx is different from the frctx.
|
|
|
|
boolean needsPositions(double[] devTX) {
|
|
return gti != null ||
|
|
(getLayoutFlags() & FLAG_HAS_POSITION_ADJUSTMENTS) != 0 ||
|
|
!matchTX(devTX, frctx);
|
|
}
|
|
|
|
// used by glyphList to get strong refs to font strikes for duration of rendering call
|
|
// if devTX matches current devTX, we're ready to go
|
|
// if we don't have multiple transforms, we're already ok
|
|
|
|
// !!! I'm not sure fontInfo works so well for glyphvector, since we have to be able to handle
|
|
// the multiple-strikes case
|
|
|
|
/*
|
|
* GlyphList calls this to set up its images data. First it calls needsPositions,
|
|
* passing the devTX, to see if it should provide us a positions array to fill.
|
|
* It only doesn't need them if we're a simple glyph vector whose frctx matches the
|
|
* devtx.
|
|
* Then it calls setupGlyphImages. If we need positions, we make sure we have our
|
|
* default positions based on the frctx first. Then we set the devTX, and use
|
|
* strikes based on it to generate the images. Finally, we fill in the positions
|
|
* array.
|
|
* If we have transforms, we delegate to gti. It depends on our having first
|
|
* initialized the positions and devTX.
|
|
*/
|
|
Object setupGlyphImages(long[] images, float[] positions, double[] devTX) {
|
|
initPositions(); // FIRST ensure we have positions based on our frctx
|
|
setRenderTransform(devTX); // THEN make sure we are using the desired devTX
|
|
|
|
if (gti != null) {
|
|
return gti.setupGlyphImages(images, positions, dtx);
|
|
}
|
|
|
|
GlyphStrike gs = getDefaultStrike();
|
|
gs.strike.getGlyphImagePtrs(glyphs, images, glyphs.length);
|
|
|
|
if (positions != null) {
|
|
if (dtx.isIdentity()) {
|
|
System.arraycopy(this.positions, 0, positions, 0, glyphs.length * 2);
|
|
} else {
|
|
dtx.transform(this.positions, 0, positions, 0, glyphs.length);
|
|
}
|
|
}
|
|
|
|
return gs;
|
|
}
|
|
|
|
//////////////////////
|
|
// StandardGlyphVector private methods
|
|
/////////////////////
|
|
|
|
// We keep translation in our frctx since getPixelBounds uses it. But
|
|
// GlyphList pulls out the translation and applies it separately, so
|
|
// we strip it out when we set the dtx. Basically nothing uses the
|
|
// translation except getPixelBounds.
|
|
|
|
// called by needsPositions, setRenderTransform
|
|
private static boolean matchTX(double[] lhs, AffineTransform rhs) {
|
|
return
|
|
lhs[0] == rhs.getScaleX() &&
|
|
lhs[1] == rhs.getShearY() &&
|
|
lhs[2] == rhs.getShearX() &&
|
|
lhs[3] == rhs.getScaleY();
|
|
}
|
|
|
|
// returns new tx if old one has translation, otherwise returns old one
|
|
private static AffineTransform getNonTranslateTX(AffineTransform tx) {
|
|
if (tx.getTranslateX() != 0 || tx.getTranslateY() != 0) {
|
|
tx = new AffineTransform(tx.getScaleX(), tx.getShearY(),
|
|
tx.getShearX(), tx.getScaleY(),
|
|
0, 0);
|
|
}
|
|
return tx;
|
|
}
|
|
|
|
private static boolean equalNonTranslateTX(AffineTransform lhs, AffineTransform rhs) {
|
|
return lhs.getScaleX() == rhs.getScaleX() &&
|
|
lhs.getShearY() == rhs.getShearY() &&
|
|
lhs.getShearX() == rhs.getShearX() &&
|
|
lhs.getScaleY() == rhs.getScaleY();
|
|
}
|
|
|
|
// called by setupGlyphImages (after needsPositions, so redundant match check?)
|
|
private void setRenderTransform(double[] devTX) {
|
|
assert(devTX.length == 4);
|
|
if (!matchTX(devTX, dtx)) {
|
|
resetDTX(new AffineTransform(devTX)); // no translation since devTX len == 4.
|
|
}
|
|
}
|
|
|
|
// called by getGlyphsPixelBounds
|
|
private void setDTX(AffineTransform tx) {
|
|
if (!equalNonTranslateTX(dtx, tx)) {
|
|
resetDTX(getNonTranslateTX(tx));
|
|
}
|
|
}
|
|
|
|
// called by most functions
|
|
private void setFRCTX() {
|
|
if (!equalNonTranslateTX(frctx, dtx)) {
|
|
resetDTX(getNonTranslateTX(frctx));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the dtx for the strike refs we use. Keeps a reference to the at. At
|
|
* must not contain translation.
|
|
* Called by setRenderTransform, setDTX, initFontData.
|
|
*/
|
|
private void resetDTX(AffineTransform at) {
|
|
fsref = null;
|
|
dtx = at;
|
|
invdtx = null;
|
|
if (!dtx.isIdentity()) {
|
|
try {
|
|
invdtx = dtx.createInverse();
|
|
}
|
|
catch (NoninvertibleTransformException e) {
|
|
// we needn't care for rendering
|
|
}
|
|
}
|
|
if (gti != null) {
|
|
gti.strikesRef = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility used by getStandardGV.
|
|
* Constructs a StandardGlyphVector from a generic glyph vector.
|
|
* Do not call this from new contexts without considering the comment
|
|
* about "userGlyphs".
|
|
*/
|
|
private StandardGlyphVector(GlyphVector gv, FontRenderContext frc) {
|
|
this.font = gv.getFont();
|
|
this.frc = frc;
|
|
initFontData();
|
|
|
|
int nGlyphs = gv.getNumGlyphs();
|
|
this.userGlyphs = gv.getGlyphCodes(0, nGlyphs, null);
|
|
if (gv instanceof StandardGlyphVector) {
|
|
/* userGlyphs will be OK because this is a private constructor
|
|
* and the returned instance is used only for rendering.
|
|
* It's not constructable by user code, nor returned to the
|
|
* application. So we know "userGlyphs" are valid as having
|
|
* been either already validated or are the result of layout.
|
|
*/
|
|
this.glyphs = userGlyphs;
|
|
} else {
|
|
this.glyphs = getValidatedGlyphs(this.userGlyphs);
|
|
}
|
|
this.flags = gv.getLayoutFlags() & FLAG_MASK;
|
|
|
|
if ((flags & FLAG_HAS_POSITION_ADJUSTMENTS) != 0) {
|
|
this.positions = gv.getGlyphPositions(0, nGlyphs + 1, null);
|
|
}
|
|
|
|
if ((flags & FLAG_COMPLEX_GLYPHS) != 0) {
|
|
this.charIndices = gv.getGlyphCharIndices(0, nGlyphs, null);
|
|
}
|
|
|
|
if ((flags & FLAG_HAS_TRANSFORMS) != 0) {
|
|
AffineTransform[] txs = new AffineTransform[nGlyphs]; // worst case
|
|
for (int i = 0; i < nGlyphs; ++i) {
|
|
txs[i] = gv.getGlyphTransform(i); // gv doesn't have getGlyphsTransforms
|
|
}
|
|
|
|
setGlyphTransforms(txs);
|
|
}
|
|
}
|
|
|
|
/* Before asking the Font we see if the glyph code is
|
|
* FFFE or FFFF which are special values that we should be internally
|
|
* ready to handle as meaning invisible glyphs. The Font would report
|
|
* those as the missing glyph.
|
|
*/
|
|
int[] getValidatedGlyphs(int[] oglyphs) {
|
|
int len = oglyphs.length;
|
|
int[] vglyphs = new int[len];
|
|
for (int i=0; i<len; i++) {
|
|
if (oglyphs[i] == 0xFFFE || oglyphs[i] == 0xFFFF) {
|
|
vglyphs[i] = oglyphs[i];
|
|
} else {
|
|
vglyphs[i] = font2D.getValidatedGlyphCode(oglyphs[i]);
|
|
}
|
|
}
|
|
return vglyphs;
|
|
}
|
|
|
|
// utility used by constructors
|
|
private void init(Font font, char[] text, int start, int count,
|
|
FontRenderContext frc, int flags) {
|
|
|
|
if (start < 0 || count < 0 || start + count > text.length) {
|
|
throw new ArrayIndexOutOfBoundsException("start or count out of bounds");
|
|
}
|
|
|
|
this.font = font;
|
|
this.frc = frc;
|
|
this.flags = flags;
|
|
|
|
if (getTracking(font) != 0) {
|
|
addFlags(FLAG_HAS_POSITION_ADJUSTMENTS);
|
|
}
|
|
|
|
// !!! change mapper interface?
|
|
if (start != 0) {
|
|
char[] temp = new char[count];
|
|
System.arraycopy(text, start, temp, 0, count);
|
|
text = temp;
|
|
}
|
|
|
|
initFontData(); // sets up font2D
|
|
|
|
// !!! no layout for now, should add checks
|
|
// !!! need to support creating a StandardGlyphVector from a TextMeasurer's info...
|
|
glyphs = new int[count]; // hmmm
|
|
/* Glyphs obtained here are already validated by the font */
|
|
userGlyphs = glyphs;
|
|
font2D.getMapper().charsToGlyphs(count, text, glyphs);
|
|
}
|
|
|
|
private void initFontData() {
|
|
font2D = FontUtilities.getFont2D(font);
|
|
if (font2D instanceof FontSubstitution) {
|
|
font2D = ((FontSubstitution)font2D).getCompositeFont2D();
|
|
}
|
|
float s = font.getSize2D();
|
|
if (font.isTransformed()) {
|
|
ftx = font.getTransform();
|
|
if (ftx.getTranslateX() != 0 || ftx.getTranslateY() != 0) {
|
|
addFlags(FLAG_HAS_POSITION_ADJUSTMENTS);
|
|
}
|
|
ftx.setTransform(ftx.getScaleX(), ftx.getShearY(), ftx.getShearX(), ftx.getScaleY(), 0, 0);
|
|
ftx.scale(s, s);
|
|
} else {
|
|
ftx = AffineTransform.getScaleInstance(s, s);
|
|
}
|
|
|
|
frctx = frc.getTransform();
|
|
resetDTX(getNonTranslateTX(frctx));
|
|
}
|
|
|
|
/**
|
|
* Copy glyph position data into a result array starting at the indicated
|
|
* offset in the array. If the passed-in result array is null, a new
|
|
* array will be allocated and returned.
|
|
*
|
|
* This is an internal method and does no extra argument checking.
|
|
*
|
|
* @param start the index of the first glyph to get
|
|
* @param count the number of glyphs to get
|
|
* @param offset the offset into result at which to put the data
|
|
* @param result an array to hold the x,y positions
|
|
* @return the modified position array
|
|
*/
|
|
private float[] internalGetGlyphPositions(int start, int count, int offset, float[] result) {
|
|
if (result == null) {
|
|
result = new float[offset + count * 2];
|
|
}
|
|
|
|
initPositions();
|
|
|
|
// System.arraycopy is slow for stuff like this
|
|
for (int i = offset, e = offset + count * 2, p = start * 2; i < e; ++i, ++p) {
|
|
result[i] = positions[p];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private Rectangle2D getGlyphOutlineBounds(int ix) {
|
|
setFRCTX();
|
|
initPositions();
|
|
return getGlyphStrike(ix).getGlyphOutlineBounds(glyphs[ix], positions[ix*2], positions[ix*2+1]);
|
|
}
|
|
|
|
/**
|
|
* Used by getOutline, getGlyphsOutline
|
|
*/
|
|
private Shape getGlyphsOutline(int start, int count, float x, float y) {
|
|
setFRCTX();
|
|
initPositions();
|
|
|
|
GeneralPath result = new GeneralPath(GeneralPath.WIND_NON_ZERO);
|
|
for (int i = start, e = start + count, n = start * 2; i < e; ++i, n += 2) {
|
|
float px = x + positions[n];
|
|
float py = y + positions[n+1];
|
|
|
|
getGlyphStrike(i).appendGlyphOutline(glyphs[i], result, px, py);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private Rectangle getGlyphsPixelBounds(FontRenderContext frc, float x, float y, int start, int count) {
|
|
initPositions(); // FIRST ensure we have positions based on our frctx
|
|
|
|
AffineTransform tx = null;
|
|
if (frc == null || frc.equals(this.frc)) {
|
|
tx = frctx;
|
|
} else {
|
|
tx = frc.getTransform();
|
|
}
|
|
setDTX(tx); // need to get the right strikes, but we use tx itself to translate the points
|
|
|
|
if (gti != null) {
|
|
return gti.getGlyphsPixelBounds(tx, x, y, start, count);
|
|
}
|
|
|
|
FontStrike fs = getDefaultStrike().strike;
|
|
Rectangle result = null;
|
|
Rectangle r = new Rectangle();
|
|
Point2D.Float pt = new Point.Float();
|
|
int n = start * 2;
|
|
while (--count >= 0) {
|
|
pt.x = x + positions[n++];
|
|
pt.y = y + positions[n++];
|
|
tx.transform(pt, pt);
|
|
fs.getGlyphImageBounds(glyphs[start++], pt, r);
|
|
if (!r.isEmpty()) {
|
|
if (result == null) {
|
|
result = new Rectangle(r);
|
|
} else {
|
|
result.add(r);
|
|
}
|
|
}
|
|
}
|
|
return result != null ? result : r;
|
|
}
|
|
|
|
private void clearCaches(int ix) {
|
|
if (lbcacheRef != null) {
|
|
Shape[] lbcache = lbcacheRef.get();
|
|
if (lbcache != null) {
|
|
lbcache[ix] = null;
|
|
}
|
|
}
|
|
|
|
if (vbcacheRef != null) {
|
|
Shape[] vbcache = vbcacheRef.get();
|
|
if (vbcache != null) {
|
|
vbcache[ix] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void clearCaches() {
|
|
lbcacheRef = null;
|
|
vbcacheRef = null;
|
|
}
|
|
|
|
// internal use only for possible future extension
|
|
|
|
/**
|
|
* A flag used with getLayoutFlags that indicates whether this {@code GlyphVector} uses
|
|
* a vertical baseline.
|
|
*/
|
|
public static final int FLAG_USES_VERTICAL_BASELINE = 128;
|
|
|
|
/**
|
|
* A flag used with getLayoutFlags that indicates whether this {@code GlyphVector} uses
|
|
* vertical glyph metrics. A {@code GlyphVector} can use vertical metrics on a
|
|
* horizontal line, or vice versa.
|
|
*/
|
|
public static final int FLAG_USES_VERTICAL_METRICS = 256;
|
|
|
|
/**
|
|
* A flag used with getLayoutFlags that indicates whether this {@code GlyphVector} uses
|
|
* the 'alternate orientation.' Glyphs have a default orientation given a
|
|
* particular baseline and metrics orientation, this is the orientation appropriate
|
|
* for left-to-right text. For example, the letter 'A' can have four orientations,
|
|
* with the point at 12, 3, 6, or 9 'o clock. The following table shows where the
|
|
* point displays for different values of vertical baseline (vb), vertical
|
|
* metrics (vm) and alternate orientation (fo):<br>
|
|
* <blockquote>
|
|
* vb vm ao
|
|
* -- -- -- --
|
|
* f f f 12 ^ horizontal metrics on horizontal lines
|
|
* f f t 6 v
|
|
* f t f 9 < vertical metrics on horizontal lines
|
|
* f t t 3 >
|
|
* t f f 3 > horizontal metrics on vertical lines
|
|
* t f t 9 <
|
|
* t t f 12 ^ vertical metrics on vertical lines
|
|
* t t t 6 v
|
|
* </blockquote>
|
|
*/
|
|
public static final int FLAG_USES_ALTERNATE_ORIENTATION = 512;
|
|
|
|
|
|
/**
|
|
* Ensure that the positions array exists and holds position data.
|
|
* If the array is null, this allocates it and sets default positions.
|
|
*/
|
|
private void initPositions() {
|
|
if (positions == null) {
|
|
setFRCTX();
|
|
|
|
positions = new float[glyphs.length * 2 + 2];
|
|
|
|
Point2D.Float trackPt = null;
|
|
float track = getTracking(font);
|
|
if (track != 0) {
|
|
track *= font.getSize2D();
|
|
trackPt = new Point2D.Float(track, 0); // advance delta
|
|
}
|
|
|
|
Point2D.Float pt = new Point2D.Float(0, 0);
|
|
if (font.isTransformed()) {
|
|
AffineTransform at = font.getTransform();
|
|
at.transform(pt, pt);
|
|
positions[0] = pt.x;
|
|
positions[1] = pt.y;
|
|
|
|
if (trackPt != null) {
|
|
at.deltaTransform(trackPt, trackPt);
|
|
}
|
|
}
|
|
for (int i = 0, n = 2; i < glyphs.length; ++i, n += 2) {
|
|
getGlyphStrike(i).addDefaultGlyphAdvance(glyphs[i], pt);
|
|
if (trackPt != null) {
|
|
pt.x += trackPt.x;
|
|
pt.y += trackPt.y;
|
|
}
|
|
positions[n] = pt.x;
|
|
positions[n+1] = pt.y;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* OR newFlags with existing flags. First computes existing flags if needed.
|
|
*/
|
|
private void addFlags(int newflags) {
|
|
flags = getLayoutFlags() | newflags;
|
|
}
|
|
|
|
/**
|
|
* AND the complement of clearedFlags with existing flags. First computes existing flags if needed.
|
|
*/
|
|
private void clearFlags(int clearedFlags) {
|
|
flags = getLayoutFlags() & ~clearedFlags;
|
|
}
|
|
|
|
// general utility methods
|
|
|
|
// encapsulate the test to check whether we have per-glyph transforms
|
|
private GlyphStrike getGlyphStrike(int ix) {
|
|
if (gti == null) {
|
|
return getDefaultStrike();
|
|
} else {
|
|
return gti.getStrike(ix);
|
|
}
|
|
}
|
|
|
|
// encapsulate access to cached default glyph strike
|
|
private GlyphStrike getDefaultStrike() {
|
|
GlyphStrike gs = null;
|
|
if (fsref != null) {
|
|
gs = fsref.get();
|
|
}
|
|
if (gs == null) {
|
|
gs = GlyphStrike.create(this, dtx, null);
|
|
fsref = new SoftReference<>(gs);
|
|
}
|
|
return gs;
|
|
}
|
|
|
|
|
|
/////////////////////
|
|
// Internal utility classes
|
|
/////////////////////
|
|
|
|
// !!! I have this as a separate class instead of just inside SGV,
|
|
// but I previously didn't bother. Now I'm trying this again.
|
|
// Probably still not worth it, but I'd like to keep sgv's small in the common case.
|
|
|
|
static final class GlyphTransformInfo {
|
|
StandardGlyphVector sgv; // reference back to glyph vector - yuck
|
|
int[] indices; // index into unique strikes
|
|
double[] transforms; // six doubles per unique transform, because AT is a pain to manipulate
|
|
SoftReference<GlyphStrike[]> strikesRef; // ref to unique strikes, one per transform
|
|
boolean haveAllStrikes; // true if the strike array has been filled by getStrikes().
|
|
|
|
// used when first setting a transform
|
|
GlyphTransformInfo(StandardGlyphVector sgv) {
|
|
this.sgv = sgv;
|
|
}
|
|
|
|
// used when cloning a glyph vector, need to set back link
|
|
GlyphTransformInfo(StandardGlyphVector sgv, GlyphTransformInfo rhs) {
|
|
this.sgv = sgv;
|
|
|
|
this.indices = rhs.indices == null ? null : rhs.indices.clone();
|
|
this.transforms = rhs.transforms == null ? null : rhs.transforms.clone();
|
|
this.strikesRef = null; // can't share cache, so rather than clone, we just null out
|
|
}
|
|
|
|
// used in sgv equality
|
|
public boolean equals(GlyphTransformInfo rhs) {
|
|
if (rhs == null) {
|
|
return false;
|
|
}
|
|
if (rhs == this) {
|
|
return true;
|
|
}
|
|
if (this.indices.length != rhs.indices.length) {
|
|
return false;
|
|
}
|
|
if (this.transforms.length != rhs.transforms.length) {
|
|
return false;
|
|
}
|
|
|
|
// slow since we end up processing the same transforms multiple
|
|
// times, but since transforms can be in any order, we either do
|
|
// this or create a mapping. Equality tests aren't common so
|
|
// leave it like this.
|
|
for (int i = 0; i < this.indices.length; ++i) {
|
|
int tix = this.indices[i];
|
|
int rix = rhs.indices[i];
|
|
if ((tix == 0) != (rix == 0)) {
|
|
return false;
|
|
}
|
|
if (tix != 0) {
|
|
tix *= 6;
|
|
rix *= 6;
|
|
for (int j = 6; j > 0; --j) {
|
|
if (this.indices[--tix] != rhs.indices[--rix]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// implements sgv.setGlyphTransform
|
|
void setGlyphTransform(int glyphIndex, AffineTransform newTX) {
|
|
|
|
// we store all the glyph transforms as a double array, and for each glyph there
|
|
// is an entry in the txIndices array indicating which transform to use. 0 means
|
|
// there's no transform, 1 means use the first transform (the 6 doubles at offset
|
|
// 0), 2 means use the second transform (the 6 doubles at offset 6), etc.
|
|
//
|
|
// Since this can be called multiple times, and since the number of transforms
|
|
// affects the time it takes to construct the glyphs, we try to keep the arrays as
|
|
// compact as possible, by removing transforms that are no longer used, and reusing
|
|
// transforms where we already have them.
|
|
|
|
double[] temp = new double[6];
|
|
boolean isIdentity = true;
|
|
if (newTX == null || newTX.isIdentity()) {
|
|
// Fill in temp
|
|
temp[0] = temp[3] = 1.0;
|
|
}
|
|
else {
|
|
isIdentity = false;
|
|
newTX.getMatrix(temp);
|
|
}
|
|
|
|
if (indices == null) {
|
|
if (isIdentity) { // no change
|
|
return;
|
|
}
|
|
|
|
indices = new int[sgv.glyphs.length];
|
|
indices[glyphIndex] = 1;
|
|
transforms = temp;
|
|
} else {
|
|
boolean addSlot = false; // assume we're not growing
|
|
int newIndex = -1;
|
|
if (isIdentity) {
|
|
newIndex = 0; // might shrink
|
|
} else {
|
|
addSlot = true; // assume no match
|
|
int i;
|
|
loop:
|
|
for (i = 0; i < transforms.length; i += 6) {
|
|
for (int j = 0; j < 6; ++j) {
|
|
if (transforms[i + j] != temp[j]) {
|
|
continue loop;
|
|
}
|
|
}
|
|
addSlot = false;
|
|
break;
|
|
}
|
|
newIndex = i / 6 + 1; // if no match, end of list
|
|
}
|
|
|
|
// if we're using the same transform, nothing to do
|
|
int oldIndex = indices[glyphIndex];
|
|
if (newIndex != oldIndex) {
|
|
// see if we are removing last use of the old slot
|
|
boolean removeSlot = false;
|
|
if (oldIndex != 0) {
|
|
removeSlot = true;
|
|
for (int i = 0; i < indices.length; ++i) {
|
|
if (indices[i] == oldIndex && i != glyphIndex) {
|
|
removeSlot = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (removeSlot && addSlot) { // reuse old slot with new transform
|
|
newIndex = oldIndex;
|
|
System.arraycopy(temp, 0, transforms, (newIndex - 1) * 6, 6);
|
|
} else if (removeSlot) {
|
|
if (transforms.length == 6) { // removing last one, so clear arrays
|
|
indices = null;
|
|
transforms = null;
|
|
|
|
sgv.clearCaches(glyphIndex);
|
|
sgv.clearFlags(FLAG_HAS_TRANSFORMS);
|
|
strikesRef = null;
|
|
|
|
return;
|
|
}
|
|
|
|
double[] ttemp = new double[transforms.length - 6];
|
|
System.arraycopy(transforms, 0, ttemp, 0, (oldIndex - 1) * 6);
|
|
System.arraycopy(transforms, oldIndex * 6, ttemp, (oldIndex - 1) * 6,
|
|
transforms.length - oldIndex * 6);
|
|
transforms = ttemp;
|
|
|
|
// clean up indices
|
|
for (int i = 0; i < indices.length; ++i) {
|
|
if (indices[i] > oldIndex) { // ignore == oldIndex, it's going away
|
|
indices[i] -= 1;
|
|
}
|
|
}
|
|
if (newIndex > oldIndex) { // don't forget to decrement this too if we need to
|
|
--newIndex;
|
|
}
|
|
} else if (addSlot) {
|
|
double[] ttemp = new double[transforms.length + 6];
|
|
System.arraycopy(transforms, 0, ttemp, 0, transforms.length);
|
|
System.arraycopy(temp, 0, ttemp, transforms.length, 6);
|
|
transforms = ttemp;
|
|
}
|
|
|
|
indices[glyphIndex] = newIndex;
|
|
}
|
|
}
|
|
|
|
sgv.clearCaches(glyphIndex);
|
|
sgv.addFlags(FLAG_HAS_TRANSFORMS);
|
|
strikesRef = null;
|
|
}
|
|
|
|
// implements sgv.getGlyphTransform
|
|
AffineTransform getGlyphTransform(int ix) {
|
|
int index = indices[ix];
|
|
if (index == 0) {
|
|
return null;
|
|
}
|
|
|
|
int x = (index - 1) * 6;
|
|
return new AffineTransform(transforms[x + 0],
|
|
transforms[x + 1],
|
|
transforms[x + 2],
|
|
transforms[x + 3],
|
|
transforms[x + 4],
|
|
transforms[x + 5]);
|
|
}
|
|
|
|
int transformCount() {
|
|
if (transforms == null) {
|
|
return 0;
|
|
}
|
|
return transforms.length / 6;
|
|
}
|
|
|
|
/**
|
|
* The strike cache works like this.
|
|
*
|
|
* -Each glyph is thought of as having a transform, usually identity.
|
|
* -Each request for a strike is based on a device transform, either the
|
|
* one in the frc or the rendering transform.
|
|
* -For general info, strikes are held with soft references.
|
|
* -When rendering, strikes must be held with hard references for the
|
|
* duration of the rendering call. GlyphList will have to hold this
|
|
* info along with the image and position info, but toss the strike info
|
|
* when done.
|
|
* -Build the strike cache as needed. If the dev transform we want to use
|
|
* has changed from the last time it is built, the cache is flushed by
|
|
* the caller before these methods are called.
|
|
*
|
|
* Use a tx that doesn't include translation components of dst tx.
|
|
*/
|
|
Object setupGlyphImages(long[] images, float[] positions, AffineTransform tx) {
|
|
int len = sgv.glyphs.length;
|
|
|
|
GlyphStrike[] sl = getAllStrikes();
|
|
for (int i = 0; i < len; ++i) {
|
|
GlyphStrike gs = sl[indices[i]];
|
|
int glyphID = sgv.glyphs[i];
|
|
images[i] = gs.strike.getGlyphImagePtr(glyphID);
|
|
|
|
gs.getGlyphPosition(glyphID, i*2, sgv.positions, positions);
|
|
}
|
|
tx.transform(positions, 0, positions, 0, len);
|
|
|
|
return sl;
|
|
}
|
|
|
|
Rectangle getGlyphsPixelBounds(AffineTransform tx, float x, float y, int start, int count) {
|
|
Rectangle result = null;
|
|
Rectangle r = new Rectangle();
|
|
Point2D.Float pt = new Point.Float();
|
|
int n = start * 2;
|
|
while (--count >= 0) {
|
|
GlyphStrike gs = getStrike(start);
|
|
pt.x = x + sgv.positions[n++] + gs.dx;
|
|
pt.y = y + sgv.positions[n++] + gs.dy;
|
|
tx.transform(pt, pt);
|
|
gs.strike.getGlyphImageBounds(sgv.glyphs[start++], pt, r);
|
|
if (!r.isEmpty()) {
|
|
if (result == null) {
|
|
result = new Rectangle(r);
|
|
} else {
|
|
result.add(r);
|
|
}
|
|
}
|
|
}
|
|
return result != null ? result : r;
|
|
}
|
|
|
|
GlyphStrike getStrike(int glyphIndex) {
|
|
if (indices != null) {
|
|
GlyphStrike[] strikes = getStrikeArray();
|
|
return getStrikeAtIndex(strikes, indices[glyphIndex]);
|
|
}
|
|
return sgv.getDefaultStrike();
|
|
}
|
|
|
|
private GlyphStrike[] getAllStrikes() {
|
|
if (indices == null) {
|
|
return null;
|
|
}
|
|
|
|
GlyphStrike[] strikes = getStrikeArray();
|
|
if (!haveAllStrikes) {
|
|
for (int i = 0; i < strikes.length; ++i) {
|
|
getStrikeAtIndex(strikes, i);
|
|
}
|
|
haveAllStrikes = true;
|
|
}
|
|
|
|
return strikes;
|
|
}
|
|
|
|
private GlyphStrike[] getStrikeArray() {
|
|
GlyphStrike[] strikes = null;
|
|
if (strikesRef != null) {
|
|
strikes = strikesRef.get();
|
|
}
|
|
if (strikes == null) {
|
|
haveAllStrikes = false;
|
|
strikes = new GlyphStrike[transformCount() + 1];
|
|
strikesRef = new SoftReference<>(strikes);
|
|
}
|
|
|
|
return strikes;
|
|
}
|
|
|
|
private GlyphStrike getStrikeAtIndex(GlyphStrike[] strikes, int strikeIndex) {
|
|
GlyphStrike strike = strikes[strikeIndex];
|
|
if (strike == null) {
|
|
if (strikeIndex == 0) {
|
|
strike = sgv.getDefaultStrike();
|
|
} else {
|
|
int ix = (strikeIndex - 1) * 6;
|
|
AffineTransform gtx = new AffineTransform(transforms[ix],
|
|
transforms[ix+1],
|
|
transforms[ix+2],
|
|
transforms[ix+3],
|
|
transforms[ix+4],
|
|
transforms[ix+5]);
|
|
|
|
strike = GlyphStrike.create(sgv, sgv.dtx, gtx);
|
|
}
|
|
strikes[strikeIndex] = strike;
|
|
}
|
|
return strike;
|
|
}
|
|
}
|
|
|
|
// This adjusts the metrics by the translation components of the glyph
|
|
// transform. It is done here since the translation is not known by the
|
|
// strike.
|
|
// It adjusts the position of the image and the advance.
|
|
|
|
public static final class GlyphStrike {
|
|
StandardGlyphVector sgv;
|
|
FontStrike strike; // hard reference
|
|
float dx;
|
|
float dy;
|
|
|
|
static GlyphStrike create(StandardGlyphVector sgv, AffineTransform dtx, AffineTransform gtx) {
|
|
float dx = 0;
|
|
float dy = 0;
|
|
|
|
AffineTransform tx = sgv.ftx;
|
|
if (!dtx.isIdentity() || gtx != null) {
|
|
tx = new AffineTransform(sgv.ftx);
|
|
if (gtx != null) {
|
|
tx.preConcatenate(gtx);
|
|
dx = (float)tx.getTranslateX(); // uses ftx then gtx to get translation
|
|
dy = (float)tx.getTranslateY();
|
|
}
|
|
if (!dtx.isIdentity()) {
|
|
tx.preConcatenate(dtx);
|
|
}
|
|
}
|
|
|
|
int ptSize = 1; // only matters for 'gasp' case.
|
|
Object aaHint = sgv.frc.getAntiAliasingHint();
|
|
if (aaHint == VALUE_TEXT_ANTIALIAS_GASP) {
|
|
/* Must pass in the calculated point size for rendering.
|
|
* If the glyph tx is anything other than identity or a
|
|
* simple translate, calculate the transformed point size.
|
|
*/
|
|
if (!tx.isIdentity() &&
|
|
(tx.getType() & ~AffineTransform.TYPE_TRANSLATION) != 0) {
|
|
double shearx = tx.getShearX();
|
|
if (shearx != 0) {
|
|
double scaley = tx.getScaleY();
|
|
ptSize =
|
|
(int)Math.sqrt(shearx * shearx + scaley * scaley);
|
|
} else {
|
|
ptSize = (int)(Math.abs(tx.getScaleY()));
|
|
}
|
|
}
|
|
}
|
|
int aa = FontStrikeDesc.getAAHintIntVal(aaHint,sgv.font2D, ptSize);
|
|
int fm = FontStrikeDesc.getFMHintIntVal
|
|
(sgv.frc.getFractionalMetricsHint());
|
|
FontStrikeDesc desc = new FontStrikeDesc(dtx,
|
|
tx,
|
|
sgv.font.getStyle(),
|
|
aa, fm);
|
|
// Get the strike via the handle. Shouldn't matter
|
|
// if we've invalidated the font but its an extra precaution.
|
|
// do we want the CompFont from CFont here ?
|
|
Font2D f2d = sgv.font2D;
|
|
if (f2d instanceof FontSubstitution) {
|
|
f2d = ((FontSubstitution)f2d).getCompositeFont2D();
|
|
}
|
|
FontStrike strike = f2d.handle.font2D.getStrike(desc); // !!! getStrike(desc, false)
|
|
|
|
return new GlyphStrike(sgv, strike, dx, dy);
|
|
}
|
|
|
|
private GlyphStrike(StandardGlyphVector sgv, FontStrike strike, float dx, float dy) {
|
|
this.sgv = sgv;
|
|
this.strike = strike;
|
|
this.dx = dx;
|
|
this.dy = dy;
|
|
}
|
|
|
|
void getADL(ADL result) {
|
|
StrikeMetrics sm = strike.getFontMetrics();
|
|
Point2D.Float delta = null;
|
|
if (sgv.font.isTransformed()) {
|
|
delta = new Point2D.Float();
|
|
delta.x = (float)sgv.font.getTransform().getTranslateX();
|
|
delta.y = (float)sgv.font.getTransform().getTranslateY();
|
|
}
|
|
|
|
result.ascentX = -sm.ascentX;
|
|
result.ascentY = -sm.ascentY;
|
|
result.descentX = sm.descentX;
|
|
result.descentY = sm.descentY;
|
|
result.leadingX = sm.leadingX;
|
|
result.leadingY = sm.leadingY;
|
|
}
|
|
|
|
void getGlyphPosition(int glyphID, int ix, float[] positions, float[] result) {
|
|
result[ix] = positions[ix] + dx;
|
|
++ix;
|
|
result[ix] = positions[ix] + dy;
|
|
}
|
|
|
|
void addDefaultGlyphAdvance(int glyphID, Point2D.Float result) {
|
|
// !!! change this API? Creates unnecessary garbage. Also the name doesn't quite fit.
|
|
// strike.addGlyphAdvance(Point2D.Float adv); // hey, whaddya know, matches my api :-)
|
|
Point2D.Float adv = strike.getGlyphMetrics(glyphID);
|
|
result.x += adv.x + dx;
|
|
result.y += adv.y + dy;
|
|
}
|
|
|
|
Rectangle2D getGlyphOutlineBounds(int glyphID, float x, float y) {
|
|
Rectangle2D result = null;
|
|
if (sgv.invdtx == null) {
|
|
result = new Rectangle2D.Float();
|
|
result.setRect(strike.getGlyphOutlineBounds(glyphID)); // don't mutate cached rect
|
|
} else {
|
|
GeneralPath gp = strike.getGlyphOutline(glyphID, 0, 0);
|
|
gp.transform(sgv.invdtx);
|
|
result = gp.getBounds2D();
|
|
}
|
|
/* Since x is the logical advance of the glyph to this point.
|
|
* Because of the way that Rectangle.union is specified, this
|
|
* means that subsequent unioning of a rect including that
|
|
* will be affected, even if the glyph is empty. So skip such
|
|
* cases. This alone isn't a complete solution since x==0
|
|
* may also not be what is wanted. The code that does the
|
|
* unioning also needs to be aware to ignore empty glyphs.
|
|
*/
|
|
if (!result.isEmpty()) {
|
|
result.setRect(result.getMinX() + x + dx,
|
|
result.getMinY() + y + dy,
|
|
result.getWidth(), result.getHeight());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void appendGlyphOutline(int glyphID, GeneralPath result, float x, float y) {
|
|
// !!! fontStrike needs a method for this. For that matter, GeneralPath does.
|
|
GeneralPath gp = null;
|
|
if (sgv.invdtx == null) {
|
|
gp = strike.getGlyphOutline(glyphID, x + dx, y + dy);
|
|
} else {
|
|
gp = strike.getGlyphOutline(glyphID, 0, 0);
|
|
gp.transform(sgv.invdtx);
|
|
gp.transform(AffineTransform.getTranslateInstance(x + dx, y + dy));
|
|
}
|
|
PathIterator iterator = gp.getPathIterator(null);
|
|
result.append(iterator, false);
|
|
}
|
|
}
|
|
|
|
public String toString() {
|
|
return appendString(null).toString();
|
|
}
|
|
|
|
StringBuffer appendString(StringBuffer buf) {
|
|
if (buf == null) {
|
|
buf = new StringBuffer();
|
|
}
|
|
try {
|
|
buf.append("SGV{font: ");
|
|
buf.append(font.toString());
|
|
buf.append(", frc: ");
|
|
buf.append(frc.toString());
|
|
buf.append(", glyphs: (");
|
|
buf.append(glyphs.length);
|
|
buf.append(")[");
|
|
for (int i = 0; i < glyphs.length; ++i) {
|
|
if (i > 0) {
|
|
buf.append(", ");
|
|
}
|
|
buf.append(Integer.toHexString(glyphs[i]));
|
|
}
|
|
buf.append("]");
|
|
if (positions != null) {
|
|
buf.append(", positions: (");
|
|
buf.append(positions.length);
|
|
buf.append(")[");
|
|
for (int i = 0; i < positions.length; i += 2) {
|
|
if (i > 0) {
|
|
buf.append(", ");
|
|
}
|
|
buf.append(positions[i]);
|
|
buf.append("@");
|
|
buf.append(positions[i+1]);
|
|
}
|
|
buf.append("]");
|
|
}
|
|
if (charIndices != null) {
|
|
buf.append(", indices: (");
|
|
buf.append(charIndices.length);
|
|
buf.append(")[");
|
|
for (int i = 0; i < charIndices.length; ++i) {
|
|
if (i > 0) {
|
|
buf.append(", ");
|
|
}
|
|
buf.append(charIndices[i]);
|
|
}
|
|
buf.append("]");
|
|
}
|
|
buf.append(", flags:");
|
|
if (getLayoutFlags() == 0) {
|
|
buf.append(" default");
|
|
} else {
|
|
if ((flags & FLAG_HAS_TRANSFORMS) != 0) {
|
|
buf.append(" tx");
|
|
}
|
|
if ((flags & FLAG_HAS_POSITION_ADJUSTMENTS) != 0) {
|
|
buf.append(" pos");
|
|
}
|
|
if ((flags & FLAG_RUN_RTL) != 0) {
|
|
buf.append(" rtl");
|
|
}
|
|
if ((flags & FLAG_COMPLEX_GLYPHS) != 0) {
|
|
buf.append(" complex");
|
|
}
|
|
}
|
|
}
|
|
catch(Exception e) {
|
|
buf.append(' ').append(e.getMessage());
|
|
}
|
|
buf.append('}');
|
|
|
|
return buf;
|
|
}
|
|
|
|
static class ADL {
|
|
public float ascentX;
|
|
public float ascentY;
|
|
public float descentX;
|
|
public float descentY;
|
|
public float leadingX;
|
|
public float leadingY;
|
|
|
|
public String toString() {
|
|
return toStringBuffer(null).toString();
|
|
}
|
|
|
|
protected StringBuffer toStringBuffer(StringBuffer result) {
|
|
if (result == null) {
|
|
result = new StringBuffer();
|
|
}
|
|
result.append("ax: ");
|
|
result.append(ascentX);
|
|
result.append(" ay: ");
|
|
result.append(ascentY);
|
|
result.append(" dx: ");
|
|
result.append(descentX);
|
|
result.append(" dy: ");
|
|
result.append(descentY);
|
|
result.append(" lx: ");
|
|
result.append(leadingX);
|
|
result.append(" ly: ");
|
|
result.append(leadingY);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|