/*
* Copyright 2014-15 Skynav, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY SKYNAV, INC. AND ITS CONTRIBUTORS “AS IS” AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL SKYNAV, INC. OR ITS CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.skynav.ttpe.fonts;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.io.File;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.IntBuffer;
import java.text.MessageFormat;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import org.apache.fontbox.cff.CFFCIDFont;
import org.apache.fontbox.cff.CFFCharset;
import org.apache.fontbox.cff.CFFFont;
import org.apache.fontbox.cff.Type1CharString;
import org.apache.fontbox.ttf.CFFTable;
import org.apache.fontbox.ttf.CmapSubtable;
import org.apache.fontbox.ttf.GlyphData;
import org.apache.fontbox.ttf.GlyphTable;
import org.apache.fontbox.ttf.KerningSubtable;
import org.apache.fontbox.ttf.KerningTable;
import org.apache.fontbox.ttf.NamingTable;
import org.apache.fontbox.ttf.OS2WindowsMetricsTable;
import org.apache.fontbox.ttf.OTFParser;
import org.apache.fontbox.ttf.OpenTypeFont;
import org.apache.fontbox.ttf.advanced.GlyphDefinitionTable;
import org.apache.fontbox.ttf.advanced.GlyphPositioningTable;
import org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable;
import org.apache.fontbox.ttf.advanced.util.CharAssociation;
import org.apache.fontbox.ttf.advanced.util.CharNormalize;
import org.apache.fontbox.ttf.advanced.util.CharScript;
import org.apache.fontbox.ttf.advanced.util.GlyphSequence;
import com.skynav.ttpe.geometry.Axis;
import com.skynav.ttpe.util.Characters;
import com.skynav.ttv.util.Reporter;
@SuppressWarnings({"unchecked","rawtypes"})
public class FontState {
private static final MessageFormat doubleFormatter = new MessageFormat("{0,number,#.####}", Locale.US);
private static final int MISSING_GLYPH_CHAR = '#'; // character for missing glyph
private static final int PUA_LOWER_LIMIT = 0xE000; // lower limit (inclusive) of bmp pua
private static final int PUA_UPPER_LIMIT = 0xF8FF; // upper limit (inclusive) of bmp pua
private static final SortedSet<FontFeature> RVS_M1; // feature set (reverse, with mirror)
private static final SortedSet<FontFeature> RVS_M0; // feature set (reverse, without mirror)
static {
SortedSet<FontFeature> ss;
ss = new java.util.TreeSet<FontFeature>();
ss.add(FontFeature.REVS.parameterize(true));
ss.add(FontFeature.MIRR.parameterize(true));
RVS_M1 = Collections.unmodifiableSortedSet(ss);
ss = new java.util.TreeSet<FontFeature>();
ss.add(FontFeature.REVS.parameterize(true));
ss.add(FontFeature.MIRR.parameterize(false));
RVS_M0 = Collections.unmodifiableSortedSet(ss);
}
private String source; // font file source path
private BitSet forcePath; // force use of path for specified glyph indices represented by bit set
private Reporter reporter; // reporter for warnings, errors, etc
private OpenTypeFont otf; // open type font instance (from fontbox)
private boolean otfLoadFailed; // true if load of open type font failed
private NamingTable nameTable; // name table
private OS2WindowsMetricsTable os2Table; // os2 table
private CmapSubtable cmapSubtable; // cmap subtable for character to glyph mapping
private KerningSubtable kerningSubtable; // kerning subtable
private GlyphTable glyphTable; // glyph table
private CFFTable cffTable; // cff table (postscript fonts)
private boolean useLayoutTables; // true if using advanced typographic (layout) tables
private boolean useLayoutTablesFailed; // true if load of advanced typographic (layout) tables failed
private GlyphDefinitionTable gdef; // glyph definition table, if available
private GlyphSubstitutionTable gsub; // glyph substitution table, if available
private GlyphPositioningTable gpos; // glyph positioning table, if available
private Map<GlyphMapping.Key,GlyphMapping> mappings; // glyph mappings cache
private Map<Integer,Integer> gidMappings; // map from glyph indices (gids) to input character codes
private Set<Integer> gidMappingFailures; // set of input character codes that don't map to any glyph index
private int puaMappingNext; // next pua assignment for glyphs with no corresponding character code
private Map<Integer,Integer> puaMappings; // map from unmappable glyph indices to pua character codes
private Map<Integer,Integer> puaGlyphMappings; // map from pua character codes to unmappable glyph indices
private Set<Integer> puaMappingFailures; // set of glyph indices for which no pua character code could be assigned
private double upem; // units per em
private int[] widths; // array of glyph advances in horizontal axis
private int[] heights; // array of glyph advances in vertical axis
private Map<PathCacheKey,String> pathCache; // map from {font key, glyph id, advance} to glyph path string
public FontState(String source, BitSet forcePath, Reporter reporter) {
this.source = source;
this.forcePath = forcePath;
this.reporter = reporter;
this.mappings = new java.util.HashMap<GlyphMapping.Key,GlyphMapping>();
this.gidMappings = new java.util.HashMap<Integer,Integer>();
this.puaMappingNext = PUA_LOWER_LIMIT;
}
public String getPreferredFamilyName(FontKey key) {
if (maybeLoad(key)) {
if (nameTable != null) {
String name = nameTable.getName(16, 1, 0, 0);
if (name == null)
name = nameTable.getFontFamily();
return name;
} else
return key.family;
} else
return "unknown";
}
public double getLeading(FontKey key) {
if (maybeLoad(key))
return scaleFontUnits(key, getLeading());
else
return 0;
}
public int getLeading() {
int l = os2Table.getTypoLineGap();
if (l == 0) {
int wa = os2Table.getWinAscent();
int wd = os2Table.getWinDescent();
int wl = (wa + wd) - (int) upem;
if (wl > 0)
l = wl;
}
return l;
}
public double getAscent(FontKey key) {
if (maybeLoad(key))
return scaleFontUnits(key, os2Table.getTypoAscender());
else
return 0;
}
public double getDescent(FontKey key) {
if (maybeLoad(key))
return scaleFontUnits(key, os2Table.getTypoDescender());
else
return 0;
}
public GlyphMapping getGlyphMapping(FontKey key, String text, SortedSet<FontFeature> features) {
if (maybeLoad(key)) {
GlyphMapping.Key gmk = GlyphMapping.makeKey(text, features);
GlyphMapping gm = getCachedMapping(key, gmk);
if (gm == null)
gm = putCachedMapping(key, gmk, mapGlyphs(key, gmk));
return gm;
} else
return null;
}
public GlyphMapping maybeReverse(FontKey key, GlyphMapping mapping, boolean mirror) {
assert mapping != null;
GlyphMapping gm = mapping;
if (!gm.isReversed()) {
GlyphMapping.Key gmk = GlyphMapping.makeKey(mapping.getKey(), makeReversingFeatures(mirror));
gm = getCachedMapping(key, gmk);
if (gm == null)
gm = putCachedMapping(key, gmk, reverseGlyphs(key, gmk, mapping));
}
return gm;
}
public int getAdvance(FontKey key, GlyphMapping gm) {
int[] advances = gm.getAdvances();
int advance = 0;
for (int i = 0, n = advances.length; i < n; ++i) {
advance += advances[i];
}
return advance;
}
public int[] getAdvances(FontKey key, GlyphMapping gm) {
return gm.getAdvances();
}
public double getScaledAdvance(FontKey key, GlyphMapping gm) {
double[] scaledAdvances = getScaledAdvances(key, gm);
double scaledAdvance = 0;
for (int i = 0, n = scaledAdvances.length; i < n; ++i)
scaledAdvance += scaledAdvances[i];
return scaledAdvance;
}
public double[] getScaledAdvances(FontKey key, GlyphMapping gm) {
int[] advances = gm.getAdvances();
double size = key.size.getDimension(gm.getKey().getAdvanceAxis(key));
double[] scaledAdvances = new double[advances.length];
for (int i = 0, n = scaledAdvances.length; i < n; ++i)
scaledAdvances[i] = scaleFontUnits(size, advances[i]);
return scaledAdvances;
}
public double[][] getScaledAdjustments(FontKey key, GlyphMapping gm) {
int[][] adjustments = gm.getAdjustments();
if (adjustments != null) {
double size = key.size.getDimension(gm.getKey().getAdvanceAxis(key));
double[][] scaledAdjustments = new double[adjustments.length][];
for (int i = 0, n = scaledAdjustments.length; i < n; ++i) {
int[] ia = adjustments[i];
if (ia != null)
scaledAdjustments[i] = scaleFontUnits(size, ia);
}
return scaledAdjustments;
} else
return null;
}
public boolean containsPUAMapping(FontKey key, String glyphsAsText) {
for (int i = 0, n = glyphsAsText.length(); i < n; ++i) {
int c = glyphsAsText.charAt(i);
if (inActivePUARange(c))
return true;
}
return false;
}
private boolean inActivePUARange(int c) {
return (c >= PUA_LOWER_LIMIT) && (c < puaMappingNext);
}
public String getGlyphsPath(FontKey key, String glyphsAsText, Axis resolvedAxis, double[] advances) {
if (glyphTable != null)
return getGlyphsPathContours(key, glyphsAsText, advances, glyphTable);
else if (cffTable != null)
return getGlyphsPathContours(key, glyphsAsText, advances, cffTable);
else
return "";
}
private String getGlyphsPathContours(FontKey key, String glyphsAsText, double[] advances, GlyphTable glyphs) {
StringBuffer sb = new StringBuffer();
int[] retNext = new int[1];
for (int i = 0, n = glyphsAsText.length(); i < n; i = retNext[0]) {
int gi = getGlyphId(glyphsAsText, i, false, null, retNext);
if (gi > 0) {
String p = getGlyphPath(key, gi, advances[i], glyphs);
if (p != null)
sb.append(p);
}
}
return sb.toString();
}
private String getGlyphPath(FontKey key, int gi, double advance, GlyphTable glyphs) {
PathCacheKey pck = new PathCacheKey(key, gi, advance);
if (hasCachedGlyphPath(pck))
return getCachedGlyphPath(pck);
else
return putCachedGlyphPath(pck, getGlyphPath(pck, glyphs));
}
private String getGlyphPath(PathCacheKey pck, GlyphTable glyphs) {
try {
GlyphData gd = glyphs.getGlyph(pck.getGlyph());
if (gd != null) {
GeneralPath p = gd.getPath();
if (p != null)
return getGlyphPath(pck.getFontKey(), p, pck.getAdvance());
}
} catch (IOException e) {
}
return null;
}
private String getGlyphsPathContours(FontKey key, String glyphsAsText, double[] advances, CFFTable glyphs) {
StringBuffer sb = new StringBuffer();
CFFFont cff = glyphs.getFont();
if ((cff != null) && (cff instanceof CFFCIDFont)) {
CFFCharset charset = ((CFFCIDFont) cff).getCharset();
if (charset != null) {
int[] retNext = new int[1];
for (int i = 0, n = glyphsAsText.length(); i < n; i = retNext[0]) {
int gi = getGlyphId(glyphsAsText, i, false, null, retNext);
if (gi > 0) {
String p = getGlyphPath(key, gi, advances[i], cff, charset);
if (p != null)
sb.append(p);
}
}
}
}
return sb.toString();
}
private String getGlyphPath(FontKey key, int gi, double advance, CFFFont cff, CFFCharset charset) {
PathCacheKey pck = new PathCacheKey(key, gi, advance);
if (hasCachedGlyphPath(pck))
return getCachedGlyphPath(pck);
else
return putCachedGlyphPath(pck, getGlyphPath(pck, cff, charset));
}
private String getGlyphPath(PathCacheKey pck, CFFFont cff, CFFCharset charset) {
try {
int cid = charset.getCIDForGID(pck.getGlyph());
Type1CharString cs = cff.getType2CharString(cid);
if (cs != null) {
GeneralPath p = cs.getPath();
if (p != null)
return getGlyphPath(pck.getFontKey(), p, pck.getAdvance());
}
} catch (IOException e) {
}
return null;
}
private String getGlyphPath(FontKey key, GeneralPath p, double advance) {
StringBuffer sb = new StringBuffer();
double size = key.size.getHeight();
double s = size / this.upem;
AffineTransform ctm = AffineTransform.getScaleInstance(s, -s);
double[] coordinates = new double[6];
for (PathIterator pi = p.getPathIterator(ctm); !pi.isDone(); pi.next()) {
int op = pi.currentSegment(coordinates);
if (op == PathIterator.SEG_CLOSE) {
sb.append("Z ");
} else if (op == PathIterator.SEG_CUBICTO) {
sb.append("C ");
appendCoordinates(sb, coordinates, 6);
} else if (op == PathIterator.SEG_LINETO) {
sb.append("L ");
appendCoordinates(sb, coordinates, 2);
} else if (op == PathIterator.SEG_MOVETO) {
sb.append("M ");
appendCoordinates(sb, coordinates, 2);
} else if (op == PathIterator.SEG_QUADTO) {
sb.append("Q ");
appendCoordinates(sb, coordinates, 4);
} else {
}
}
return sb.toString().trim();
}
private boolean hasCachedGlyphPath(PathCacheKey pck) {
return (pathCache != null) && pathCache.containsKey(pck);
}
private String getCachedGlyphPath(PathCacheKey pck) {
return pathCache.get(pck);
}
private String putCachedGlyphPath(PathCacheKey pck, String p) {
if (pathCache == null)
pathCache = new java.util.HashMap<PathCacheKey, String>();
pathCache.put(pck, p);
return p;
}
private void appendCoordinates(StringBuffer sb, double[] coordinates, int numCoordinates) {
for (int i = 0, n = numCoordinates; i < n; ++i) {
sb.append(doubleFormatter.format(new Object[] {coordinates[i]}));
sb.append(' ');
}
}
private boolean maybeLoad(FontKey key) {
if ((otf == null) && !otfLoadFailed) {
OpenTypeFont otf = null;
NamingTable nameTable = null;
OS2WindowsMetricsTable os2Table = null;
CmapSubtable cmapSubtable = null;
KerningSubtable kerningSubtable = null;
GlyphTable glyphTable = null;
CFFTable cffTable = null;
double upem = 1000;
int[] widths = null;
int[] heights = null;
boolean useLayoutTables = false;
File f = new File(source);
try {
if (f.exists()) {
otf = new OTFParser(false, true).parse(f);
nameTable = otf.getNaming();
os2Table = otf.getOS2Windows();
cmapSubtable = otf.getUnicodeCmap();
KerningTable kerning = otf.getKerning();
if (kerning != null)
kerningSubtable = kerning.getHorizontalKerningSubtable();
if (!otf.isPostScript())
glyphTable = otf.getGlyph();
else
cffTable = otf.getCFF();
upem = (double) otf.getUnitsPerEm();
widths = otf.getAdvanceWidths();
heights = otf.getAdvanceHeights();
useLayoutTables = useLayoutTables(key, otf);
reporter.logInfo(reporter.message("*KEY*", "Loaded font instance ''{0}''", f.getAbsolutePath()));
} else {
reporter.logError(reporter.message("*KEY*", "Font instance ''{0}'' does not exist", f.getAbsolutePath()));
}
} catch (IOException e) {
reporter.logError(reporter.message("*KEY*", "Failed to load font instance ''{0}'': {1}", f.getAbsolutePath(), e.getMessage()));
}
if ((nameTable != null) && (os2Table != null) && (cmapSubtable != null)) {
this.otf = otf;
this.nameTable = nameTable;
this.os2Table = os2Table;
this.cmapSubtable = cmapSubtable;
this.kerningSubtable = kerningSubtable;
this.glyphTable = glyphTable;
this.cffTable = cffTable;
this.upem = upem;
this.widths = widths;
this.heights = heights;
this.useLayoutTables = useLayoutTables;
} else
otfLoadFailed = true;
}
return !otfLoadFailed;
}
private boolean useLayoutTables(FontKey key, OpenTypeFont otf) throws IOException
{
if (otf == null)
return false;
else if (useLayoutTables)
return true;
else if (otf.hasLayoutTables() && !useLayoutTablesFailed) {
gdef = otf.getGDEF();
gsub = otf.getGSUB();
gpos = otf.getGPOS();
if ((gsub == null) && (gpos == null))
useLayoutTablesFailed = true;
return !useLayoutTablesFailed;
} else
return false;
}
private GlyphMapping getCachedMapping(FontKey key, GlyphMapping.Key gmk) {
return this.mappings.get(gmk);
}
private GlyphMapping putCachedMapping(FontKey key, GlyphMapping.Key gmk, GlyphMapping gm) {
this.mappings.put(gmk, gm);
return gm;
}
private GlyphMapping mapGlyphs(FontKey key, GlyphMapping.Key gmk) {
if (!useLayoutTables)
return mapGlyphsSimple(key, gmk);
else
return mapGlyphsComplex(key, gmk);
}
private GlyphMapping mapGlyphsSimple(FontKey key, GlyphMapping.Key gmk) {
GlyphSequence ogs = mapCharsToGlyphs(gmk.getText(), false, null);
return new GlyphMapping(gmk, ogs, getAdvances(key, gmk, ogs), null);
}
private GlyphMapping mapGlyphsComplex(FontKey key, GlyphMapping.Key gmk) {
// extract font info, text, etc.
String text = gmk.getText();
// if script is not specified or it is specified as 'auto', then compute dominant script
String script = gmk.getScript();
if ((script == null) || script.isEmpty() || "auto".equals(script))
script = CharScript.scriptTagFromCode(CharScript.dominantScript(text));
// if language is not specified or it is specified as 'none', then assign default language
String language = gmk.getLanguage();
if ((language == null) || language.isEmpty() || "none".equals(language))
language = "dflt";
// prepare mapping features
Object[][] mappingFeatures = getMappingFeatures(new java.util.TreeSet<FontFeature>(gmk.getFeatures()));
// perform substitutions
CharSequence mcs;
List associations = new java.util.ArrayList();
boolean retainControls = false;
if (performsSubstitution())
mcs = performSubstitution(key, text, script, language, mappingFeatures, associations, retainControls);
else
mcs = text;
// perform positioning
int[][] gpa = null;
if (performsPositioning())
gpa = performPositioning(key, mcs, script, language, mappingFeatures, (int) Math.floor(key.size.getDimension(key.axis) * 1000));
// reorder combining marks
mcs = reorderCombiningMarks(key, mcs, gpa, script, language, mappingFeatures, associations);
// construct final output glyph sequence and mapping
GlyphSequence ogs = mapCharsToGlyphs(mcs, false, associations);
GlyphMapping gm = new GlyphMapping(gmk, ogs, getAdvances(key, gmk, ogs), gpa);
gm.setResolvedScript(script);
gm.setResolvedLanguage(language);
return gm;
}
private Object[][] getMappingFeatures(SortedSet<FontFeature> features) {
int nf = features.size();
Object[][] mappingFeatures = new Object[nf][];
int k = 0;
for (FontFeature f : features) {
int na = f.getArgumentCount();
Object[] fa = new Object[na + 1];
fa[0] = f.getFeature();
for (int i = 0, n = na; i < n; ++i)
fa[i + 1] = f.getArgument(i);
mappingFeatures[k++] = fa;
}
return mappingFeatures;
}
private boolean requiresMirror(Object[][] features) {
for (Object[] f : features) {
if (requiresMirror(f))
return true;
}
return false;
}
private boolean requiresMirror(Object[] feature) {
if ((feature != null) && (feature.length > 1)) {
if (feature[0] instanceof String) {
String n = (String) feature[0];
if (n.equals(FontFeature.BIDI.getFeature())) {
if (feature[1] instanceof Integer) {
Integer i = (Integer) feature[1];
return (i & 1) == 1;
}
}
}
}
return false;
}
private SortedSet<FontFeature> makeReversingFeatures(boolean mirror) {
return new java.util.TreeSet<FontFeature>(mirror ? RVS_M1 : RVS_M0);
}
private GlyphMapping reverseGlyphs(FontKey key, GlyphMapping.Key gmk, GlyphMapping gm) {
return gm.reverse(gmk);
}
private int[] getAdvances(FontKey key, GlyphMapping.Key gmk, GlyphSequence gs) {
boolean vertical = gmk.getAdvanceAxis(key).isVertical();
int[] glyphs = getGlyphs(gs);
int[] kerning = gmk.isKerningEnabled() ? ((kerningSubtable != null) ? kerningSubtable.getKerning(glyphs) : null) : null;
int[] advances = new int[glyphs.length];
for (int i = 0, n = advances.length; i < n; ++i) {
int gi = glyphs[i];
if (gi != 0) {
int c = gidMappings.get(gi);
if (!Characters.isZeroWidthWhitespace(c)) {
int a = vertical ? getAdvanceHeight(gi) : getAdvanceWidth(gi);
int k = (kerning != null) ? kerning[i] : 0;
advances[i] = a + k;
}
}
}
return advances;
}
private int[] getGlyphs(GlyphSequence gs) {
int[] glyphs = new int[gs.getGlyphCount()];
for (int i = 0, n = glyphs.length; i < n; ++i)
glyphs[i] = gs.getGlyph(i);
return glyphs;
}
private int getAdvanceHeight(int gi) {
if (heights != null) {
if (gi >= heights.length)
gi = heights.length - 1;
return heights[gi];
} else
return 0;
}
private int getAdvanceWidth(int gi) {
if (widths != null) {
if (gi >= widths.length)
gi = widths.length - 1;
return widths[gi];
} else
return 0;
}
private double scaleFontUnits(FontKey key, int v) {
return scaleFontUnits(key.size.getDimension(key.axis), (double) v);
}
private double[] scaleFontUnits(double size, int[] va) {
double[] sa = new double[va.length];
for (int i = 0, n = va.length; i < n; ++i)
sa[i] = scaleFontUnits(size, (double) va[i]);
return sa;
}
private double scaleFontUnits(double size, int v) {
return scaleFontUnits(size, (double) v);
}
private double scaleFontUnits(double size, double v) {
return (v / this.upem) * size;
}
// advanced typographic table support
private boolean performsSubstitution() {
return gsub != null;
}
private CharSequence performSubstitution(FontKey key, CharSequence cs, String script, String language, Object[][] features, List associations, boolean retainControls) {
if (gsub != null) {
CharSequence ncs = normalize(cs, associations);
GlyphSequence igs = mapCharsToGlyphs(ncs, requiresMirror(features), associations);
GlyphSequence ogs = gsub.substitute(igs, script, language, features);
if (associations != null) {
associations.clear();
associations.addAll(ogs.getAssociations());
}
if (!retainControls) {
ogs = elideControls(ogs);
}
CharSequence ocs = mapGlyphsToChars(ogs);
return ocs;
} else {
return cs;
}
}
private CharSequence reorderCombiningMarks(FontKey key, CharSequence cs, int[][] gpa, String script, String language, Object[][] features, List associations) {
if (gdef != null) {
GlyphSequence igs = mapCharsToGlyphs(cs, false, associations);
GlyphSequence ogs = gdef.reorderCombiningMarks(igs, getUnscaledWidths(igs), gpa, script, language, features);
if (associations != null) {
associations.clear();
associations.addAll(ogs.getAssociations());
}
CharSequence ocs = mapGlyphsToChars(ogs);
return ocs;
} else {
return cs;
}
}
private CharSequence normalize(CharSequence cs, List associations) {
return hasDecomposable(cs) ? decompose(cs, associations) : cs;
}
private boolean hasDecomposable(CharSequence cs) {
for (int i = 0, n = cs.length(); i < n; i++) {
int cc = cs.charAt(i);
if (CharNormalize.isDecomposable(cc)) {
return true;
}
}
return false;
}
private CharSequence decompose(CharSequence cs, List associations) {
StringBuffer sb = new StringBuffer(cs.length());
int[] daBuffer = new int[CharNormalize.maximumDecompositionLength()];
for (int i = 0, n = cs.length(); i < n; i++) {
int cc = cs.charAt(i);
int[] da = CharNormalize.decompose(cc, daBuffer);
for (int j = 0; j < da.length; j++) {
if (da[j] > 0) {
sb.append((char) da[j]);
} else {
break;
}
}
}
return sb;
}
private static GlyphSequence elideControls(GlyphSequence gs) {
if (hasElidableControl(gs)) {
int[] ca = gs.getCharacterArray(false);
IntBuffer ngb = IntBuffer.allocate(gs.getGlyphCount());
List nal = new java.util.ArrayList(gs.getGlyphCount());
for (int i = 0, n = gs.getGlyphCount(); i < n; ++i) {
CharAssociation a = gs.getAssociation(i);
int s = a.getStart();
int e = a.getEnd();
while (s < e) {
int ch = ca [ s ];
if (isElidableControl(ch)) {
break;
} else {
++s;
}
}
if (s == e) {
ngb.put(gs.getGlyph(i));
nal.add(a);
}
}
ngb.flip();
return new GlyphSequence(gs.getCharacters(), ngb, nal, gs.getPredications());
} else {
return gs;
}
}
private static boolean hasElidableControl(GlyphSequence gs) {
int[] ca = gs.getCharacterArray(false);
for (int i = 0, n = ca.length; i < n; ++i) {
int ch = ca [ i ];
if (isElidableControl(ch)) {
return true;
}
}
return false;
}
private static boolean isElidableControl(int ch) {
if (ch < 0x0020) {
return true;
} else if ((ch >= 0x80) && (ch < 0x00A0)) {
return true;
} else if ((ch >= 0x2000) && (ch <= 0x206F)) {
if ((ch >= 0x200B) && (ch <= 0x200F)) {
return true;
} else if ((ch >= 0x2028) && (ch <= 0x202E)) {
return true;
} else if ((ch >= 0x2060) && (ch <= 0x2064)) {
return true;
} else if (ch >= 0x2066) {
return true;
} else {
return false;
}
} else {
return false;
}
}
protected int[] getUnscaledWidths(GlyphSequence gs) {
int[] widths = new int[gs.getGlyphCount()];
for (int i = 0, n = widths.length; i < n; ++i) {
int g = gs.getGlyph(i);
if (g >= this.widths.length)
g = this.widths.length - 1;
widths[i] = this.widths[g];
}
return widths;
}
private boolean performsPositioning() {
return gpos != null;
}
private int[][] performPositioning(FontKey key, CharSequence cs, String script, String language, Object[][] features, int fontSize) {
if (gpos != null) {
GlyphSequence gs = mapCharsToGlyphs(cs, false, null);
int[][] adjustments = new int [ gs.getGlyphCount() ] [ 4 ];
if (gpos.position(gs, script, language, features, fontSize, this.widths, adjustments)) {
return adjustments;
} else
return null;
} else
return null;
}
private GlyphSequence mapCharsToGlyphs(CharSequence cs, boolean mirror, List associations) {
IntBuffer cb = IntBuffer.allocate(cs.length());
IntBuffer gb = IntBuffer.allocate(cs.length());
int gi;
int giMissing = getGlyphId(MISSING_GLYPH_CHAR, false);
int[] retChar = new int[1];
int[] retNext = new int[1];
for (int i = 0, n = cs.length(); i < n; i = retNext[0]) {
gi = getGlyphId(cs, i, mirror, retChar, retNext);
if (gi <= 0) {
maybeReportMappingFailure(retChar[0]);
gi = giMissing;
}
cb.put(retChar[0]);
gb.put(gi);
if (gi != 0)
gidMappings.put(gi, retChar[0]);
}
cb.flip();
gb.flip();
if ((associations != null) && (associations.size() == cs.length())) {
associations = new java.util.ArrayList(associations);
} else {
associations = null;
}
return new GlyphSequence(cb, gb, associations);
}
private int getGlyphId(CharSequence cs, int index, boolean mirror, int[] retChar, int[] retNext) {
int c = cs.charAt(index++);
if ((c >= 0xD800) && (c < 0xDC00)) {
int n = cs.length();
if (index < n) {
int sh = c;
int sl = cs.charAt(index++);
if ((sl >= 0xDC00) && (sl < 0xE000)) {
c = 0x10000 + ((sh - 0xD800) << 10) + ((sl - 0xDC00) << 0);
} else {
throw new IllegalArgumentException("ill-formed UTF-16 sequence, contains isolated high surrogate at index " + (index - 1));
}
} else {
throw new IllegalArgumentException("ill-formed UTF-16 sequence, contains isolated high surrogate at end of sequence");
}
} else if ((c >= 0xDC00) && (c < 0xE000)) {
throw new IllegalArgumentException("ill-formed UTF-16 sequence, contains isolated low surrogate at index " + (index - 1));
}
if (mirror && Characters.hasMirror(c))
c = Characters.toMirror(c);
if (retChar != null)
retChar[0] = c;
if (retNext != null)
retNext[0] = index;
return getGlyphId(c, true);
}
private int getGlyphId(int c, boolean reportMappingFailure) {
assert cmapSubtable != null;
int gid = cmapSubtable.getGlyphId(c);
if ((gid == 0) && inActivePUARange(c))
gid = getPUAGlyph(c);
if ((gid == 0) && reportMappingFailure)
maybeReportMappingFailure(c);
return gid;
}
private void maybeReportMappingFailure(int c) {
if (dontReportMappingFailure(c))
return;
if (gidMappingFailures == null)
gidMappingFailures = new java.util.HashSet<Integer>();
Integer value = Integer.valueOf(c);
if (!gidMappingFailures.contains(value)) {
reporter.logWarning(reporter.message("*KEY*", "No glyph mapping for character {0} in font resource ''{1}''.", Characters.formatCharacter(c), source));
gidMappingFailures.add(value);
}
}
private boolean dontReportMappingFailure(int c) {
if (Characters.isZeroWidthWhitespace(c))
return true;
else
return false;
}
private CharSequence mapGlyphsToChars(GlyphSequence gs) {
int ng = gs.getGlyphCount();
CharBuffer cb = CharBuffer.allocate(ng * 2);
for (int i = 0, n = ng; i < n; i++) {
int gi = gs.getGlyph(i);
int cc = findCharacterFromGlyphIndex(gi);
if ((cc == 0) || (cc > 0x10FFFF))
cc = getPUACharacter(gi);
if (cc > 0x00FFFF) {
int sh;
int sl;
cc -= 0x10000;
sh = ((cc >> 10) & 0x3FF) + 0xD800;
sl = ((cc >> 0) & 0x3FF) + 0xDC00;
cb.put((char) sh);
cb.put((char) sl);
} else {
cb.put((char) cc);
}
}
cb.flip();
return cb;
}
private int findCharacterFromGlyphIndex(int gi) {
if ((forcePath != null) && forcePath.get(gi))
return 0;
else if (gidMappings.containsKey(Integer.valueOf(gi)))
return gidMappings.get(gi);
else if (cmapSubtable != null) {
Integer c = cmapSubtable.getCharacterCode(gi);
return (c != null) ? (int) c : 0;
} else
return 0;
}
private int getPUACharacter(int gi) {
Integer v = (puaMappings != null) ? puaMappings.get(gi) : null;
if (v == null) {
int cc;
if (puaMappingNext < PUA_UPPER_LIMIT) {
cc = puaMappingNext++;
if (puaMappings == null) {
puaMappings = new java.util.HashMap<Integer,Integer>();
puaGlyphMappings = new java.util.HashMap<Integer,Integer>();
}
puaMappings.put(gi, cc);
puaGlyphMappings.put(cc, gi);
} else {
reportPUAMappingExhausted(gi);
cc = MISSING_GLYPH_CHAR;
}
return cc;
} else
return (int) v;
}
private int getPUAGlyph(int cc) {
Integer v = (puaGlyphMappings != null) ? puaGlyphMappings.get(cc) : null;
return (v != null) ? (int) v : 0;
}
private void reportPUAMappingExhausted(int gi) {
if (puaMappingFailures == null)
puaMappingFailures = new java.util.HashSet<Integer>();
Integer value = Integer.valueOf(gi);
if (!puaMappingFailures.contains(value)) {
reporter.logWarning(reporter.message("*KEY*", "No PUA mapping available for glyph {0} in font resource ''{1}''.", String.format("0x%04X", gi), source));
puaMappingFailures.add(value);
}
}
private static class PathCacheKey {
private FontKey key;
private int glyph;
private double advance;
public PathCacheKey(FontKey key, int glyph, double advance) {
this.glyph = glyph;
this.key = key;
this.advance = advance;
}
public FontKey getFontKey() {
return key;
}
public int getGlyph() {
return glyph;
}
public double getAdvance() {
return advance;
}
@Override
public int hashCode() {
int hc = 23;
hc = hc * 31 + key.hashCode();
hc = hc * 31 + Integer.valueOf(glyph).hashCode();
hc = hc * 31 + Double.valueOf(advance).hashCode();
return hc;
}
@Override
public boolean equals(Object o) {
if (o instanceof PathCacheKey) {
PathCacheKey other = (PathCacheKey) o;
if (glyph != other.glyph)
return false;
else if (advance != other.advance)
return false;
else if (!key.equals(other.key))
return false;
else
return true;
} else
return false;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append('[');
sb.append(key);
sb.append(',');
sb.append(glyph);
sb.append(',');
sb.append(advance);
sb.append(']');
return sb.toString();
}
}
}