blob: b93909895c176d02815f9a244cb9b7719e24b37c [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package flash.fonts;
import flash.swf.builder.types.ShapeBuilder;
import flash.swf.builder.types.ShapeIterator;
import flash.swf.types.GlyphEntry;
import flash.swf.types.Shape;
import flash.swf.SwfConstants;
import flash.util.Trace;
import org.apache.flex.forks.batik.svggen.font.Font;
import org.apache.flex.forks.batik.svggen.font.Glyph;
import org.apache.flex.forks.batik.svggen.font.Point;
import org.apache.flex.forks.batik.svggen.font.table.CmapFormat;
import org.apache.flex.forks.batik.svggen.font.table.CmapTable;
import org.apache.flex.forks.batik.svggen.font.table.GlyfTable;
import org.apache.flex.forks.batik.svggen.font.table.GlyphDescription;
import org.apache.flex.forks.batik.svggen.font.table.HeadTable;
import org.apache.flex.forks.batik.svggen.font.table.HheaTable;
import org.apache.flex.forks.batik.svggen.font.table.HmtxTable;
import org.apache.flex.forks.batik.svggen.font.table.NameTable;
import org.apache.flex.forks.batik.svggen.font.table.Os2Table;
import org.apache.flex.forks.batik.svggen.font.table.Table;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Map;
/**
* This implementation of FontManager uses Apache Batik to process
* TrueTypeFont Files.
*
* @author Peter Farland
*/
@SuppressWarnings("unchecked")
public class BatikFontManager extends CachedFontManager
{
private JREFontManager jreManager;
// Used to initialize the JREFontManager.
private Map initializeMap;
public static String processLocation(Object location)
{
String path = null;
if (location != null)
{
if (location instanceof URL)
{
URL url = (URL)location;
if (url.getProtocol().toLowerCase().indexOf("file") > -1)
{
path = url.getFile();
}
}
else
{
File f = new File(location.toString());
if (f.exists())
{
path = f.getAbsolutePath();
}
}
}
return path;
}
public void initialize(Map map)
{
super.initialize(map);
initializeMap = map;
}
public FontFace getEntryFromSystem(String familyName, int style, boolean useTwips)
{
FontManager manager = parent;
if (manager == null)
{
if (jreManager == null)
{
jreManager = new JREFontManager();
jreManager.initialize(initializeMap);
}
manager = jreManager;
}
return manager.getEntryFromSystem(familyName, style, useTwips);
}
protected FontSet createSetForSystemFont(String family, int style, boolean useTwips)
{
FontManager manager = parent;
if (manager == null || ! (manager instanceof CachedFontManager))
{
if (jreManager == null)
{
jreManager = new JREFontManager();
}
manager = jreManager;
}
return ((CachedFontManager)manager).createSetForSystemFont(family, style, useTwips);
}
public FontFace getEntryFromLocation(URL location, int requestedStyle, boolean useTwips)
{
// C: Batik should only do TT.
if (!location.toString().toLowerCase().endsWith(".ttf"))
{
return (parent != null) ? parent.getEntryFromLocation(location, requestedStyle, useTwips) : null;
}
FontFace entry = null;
String locationKey = location != null ? location.toExternalForm() : null;
Object fam = getFontFileCache().get(locationKey);
if (fam == null)
{
fam = createFontFromLocation(location, requestedStyle, useTwips);
}
if (fam != null)
{
String family = fam.toString();
FontSet fontSet = (FontSet)getFontCache().get(family);
// The font file cache should still have this family
// from the location fetch above...
if (fontSet != null)
{
entry = fontSet.get(requestedStyle);
}
// Finally, remember the family for this location
getFontFileCache().put(locationKey, family);
}
else if (parent != null)
{
// C: give the parent font manager a chance...
return parent.getEntryFromLocation(location, requestedStyle, useTwips);
}
return entry;
}
protected String createFontFromLocation(Object location, int requestedStyle, boolean useTwips)
{
String family = null;
String path = processLocation(location);
if (path != null)
{
try
{
// Use Batik to load the Font
Font font = Font.create(path);
if (font != null)
{
FSType type = FSType.getFSType(font);
if (! type.usableByFlex)
{
throw new UnusableFontLicense(location + "", type.description);
}
String copyright = font.getNameTable().getRecord(Table.nameCopyrightNotice);
String trademark = font.getNameTable().getRecord(Table.nameTrademark);
// BatikFontManager will try to work out the real
// style from the sub family name and update the
// style property...
BatikFontFace fontFace = new BatikFontFace(font, 0,
maxGlyphsPerFace, type, copyright, trademark,
useTwips, majorCompatibilityVersion < 3);
// From Flex 4 we just use the requested style rather than
// validating it matches the style described by the font
if (majorCompatibilityVersion > 3)
fontFace.style = requestedStyle;
family = fontFace.getFamily();
FontSet fontSet = (FontSet)getFontCache().get(family);
if (fontSet == null)
{
fontSet = new FontSet(maxFacesPerFont);
getFontCache().put(family, fontSet);
}
fontSet.put(fontFace.style, fontFace);
}
}
catch (UnusableFontLicense l)
{
throw l;
}
catch (Throwable t)
{
throw new RuntimeException("Unexpected exception encountered while reading font file '" + path + "'");
}
}
return family;
}
/*
public static class BatikRuntimeException extends RuntimeException {
public final static long serialVersionUID = -1;
public BatikRuntimeException(String s) {super(s);}
public BatikRuntimeException(Throwable e) {super(e);}
}
*/
public static class BatikFontFace extends CachedFontFace
{
static final short PLATFORM_APPLE_UNICODE = 0;
static final short PLATFORM_MACINTOSH = 1;
static final short PLATFORM_ISO = 2;
static final short PLATFORM_MICROSOFT = 3;
static final short ENCODING_UNDEFINED = 0;
static final short ENCODING_UGL = 1;
static final short ENCODING_ROMAN = 0;
public BatikFontFace(Font font, int style, int maxGlyphs, FSType fsType, String copyright, String trademark,
boolean useTwips, boolean keep201Behavior)
{
super(maxGlyphs, style, fsType, copyright, trademark, useTwips);
init(font, keep201Behavior);
}
private void init(Font font, boolean keep201Behavior)
{
numGlyphs = font.getNumGlyphs();
processCmapTable(font);
processNameTable(font);
processHeadTable(font); //Must process this before HHEA table
processHheaTable(font);
processOS2Table(font);
hmtx = (HmtxTable)font.getTable(Table.hmtx);
glyf = (GlyfTable)font.getTable(Table.glyf);
this.keep201Behavior = keep201Behavior;
}
/**
* Contains multiple mapping tables from Unicode and various 8-bit encodings to glyph ids.
* Multi-platform.
*/
private void processCmapTable(Font font)
{
CmapTable cmap = font.getCmapTable();
if (cmap != null)
{
// Decide upon a cmap table to use for our character to glyph look-up
if (forceAscii)
{
// We've been asked to use the ASCII/Macintosh cmap format
cmapFmt = cmap.getCmapFormat(Table.platformMacintosh, Table.encodingRoman);
platformID = PLATFORM_MACINTOSH;
encodingID = ENCODING_ROMAN;
}
else
{
// The default behaviour is to use the Unicode cmap encoding
cmapFmt = cmap.getCmapFormat(Table.platformMicrosoft, Table.encodingUGL);
if (cmapFmt == null)
{
// This might be a symbol font, so we'll look for an "undefined" encoding
cmapFmt = cmap.getCmapFormat(Table.platformMicrosoft, Table.encodingUndefined);
platformID = PLATFORM_MICROSOFT;
encodingID = ENCODING_UNDEFINED;
}
else
{
platformID = PLATFORM_MICROSOFT;
encodingID = ENCODING_UGL;
}
}
}
if (cmapFmt == null)
{
throw new RuntimeException("Cannot find a suitable cmap table");
}
}
/**
* Contains strings that provide general information about the font.
* Strings are multilingual and multi-platform and are identified by number.
* Some numbers have a standard meaning such as font name and copyright.
* Arbitrary numbered strings can also be present.
*/
private void processNameTable(Font font)
{
NameTable name = font.getNameTable();
if (name != null)
{
fontFamily = name.getRecord(Table.nameFontFamilyName);
subFamilyName = name.getRecord(Table.nameFontSubfamilyName);
postscriptName = name.getRecord(Table.namePostscriptName);
if (subFamilyName != null)
{
style = guessStyleFromSubFamilyName(subFamilyName);
}
}
else
{
if (Trace.font)
Trace.trace("Font " + fontFamily + " did not have an HEAD Table.");
}
}
/**
* Contains overall font metrics and checksum for font.
* Also contains font appearance (Mac). Must be called before
* reading HHEA table.
*/
private void processHeadTable(Font font)
{
HeadTable head = font.getHeadTable();
if (head != null)
{
unitsPerEm = head.getUnitsPerEm();
}
else
{
if (Trace.font)
Trace.trace("Font " + fontFamily + " did not have an HEAD Table.");
}
//Scale points to SWF fixed EM square of 1024 FUnits
emScale = SWF_EM_SQUARE / (double)unitsPerEm;
}
/**
* Contains overall horizontal metrics and caret slope.
* Also contains line spacing (Mac).
*/
private void processHheaTable(Font font)
{
HheaTable hhea = font.getHheaTable();
if (hhea != null)
{
//TODO: I'm skeptical about these values, shouldn't the x-height and baseline be taken into consideration here?!
ascent = (short)Math.rint(hhea.getAscender() * emScale * (useTwips ? SwfConstants.TWIPS_PER_PIXEL : 1));
descent = (short)Math.rint(hhea.getDescender() * emScale * (useTwips ? SwfConstants.TWIPS_PER_PIXEL : 1));
lineGap = (short)Math.rint(hhea.getLineGap() * emScale * (useTwips ? SwfConstants.TWIPS_PER_PIXEL : 1));
}
else
{
if (Trace.font)
Trace.trace("Font " + fontFamily + " did not have an HHEA Table.");
}
}
/**
* Contains line spacing, font weight, font style, codepoint ranges (codepage and Unicode)
* covered by glyphs, overall appearance, sub- and super-script support, strike out information.
*/
private void processOS2Table(Font font)
{
Os2Table os2 = font.getOS2Table();
if (os2 != null)
{
if (!forceAscii)
{
int winAscent = os2.getWinAscent();
ascent = (short)Math.rint(winAscent * emScale * (useTwips ? SwfConstants.TWIPS_PER_PIXEL : 1));
int winDescent = os2.getWinDescent();
descent = (short)Math.rint(winDescent * emScale * (useTwips ? SwfConstants.TWIPS_PER_PIXEL : 1));
int winLeading = os2.getTypoLineGap();
lineGap = (short)Math.rint(winLeading * emScale * (useTwips ? SwfConstants.TWIPS_PER_PIXEL : 1));
}
horizAdvanceX = os2.getAvgCharWidth();
panose = os2.getPanose().toString();
usFirstCharIndex = os2.getFirstCharIndex();
}
else
{
if (Trace.font)
Trace.trace("Font " + fontFamily + " did not have an OS/2 Table.");
}
}
//SWF through version 7 does not use kerning information.
/*
private void processKernTable(Font font)
{
KernTable kern = (KernTable)font.getTable(Table.kern);
if (kern != null)
kst = kern.getSubtable(0);
}
*/
public boolean canDisplay(char c)
{
return cmapFmt.mapCharCode(getCharIndex(c)) > 0;
}
public GlyphEntry getGlyphEntry(char c)
{
return (GlyphEntry)glyphCache.get(c);
}
public GlyphEntry createGlyphEntry(char c)
{
return createGlyphEntry(c, c);
}
public GlyphEntry createGlyphEntry(char c, char referenceChar)
{
int index = getCharIndex(referenceChar);
index = cmapFmt.mapCharCode(index);
Glyph glyph = getGlyph(index);
Shape s = getShapeFromGlyph(glyph);
GlyphEntry ge = new GlyphEntry();
ge.advance = (int)(getAdvance(referenceChar) * emScale * (useTwips ? SwfConstants.TWIPS_PER_PIXEL : 1));
ge.character = c;
ge.shape = s;
// Glyph bounds are not used by the Flash Player so no need to calculate
//ge.bounds = DefineShapeBuilder.getBounds(s.shapeRecords, null);
return ge;
}
public int getFirstChar()
{
if (platformID == PLATFORM_MICROSOFT && encodingID == ENCODING_UNDEFINED)
return usFirstCharIndex - 0xF000;
else
return usFirstCharIndex;
}
private int getCharIndex(char index)
{
if (platformID == PLATFORM_MICROSOFT && encodingID == ENCODING_UNDEFINED)
index += (usFirstCharIndex - (usFirstCharIndex - 0xF000));
return index;
}
private Glyph getGlyph(int index)
{
Glyph glyph = null;
GlyphDescription desc = glyf.getDescription(index);
if (desc != null)
glyph = new Glyph(glyf.getDescription(index), hmtx.getLeftSideBearing(index), hmtx.getAdvanceWidth(index));
return glyph;
}
private Shape getShapeFromGlyph(Glyph glyph)
{
ShapeBuilder shape = new ShapeBuilder(useTwips);
shape.setCurrentLineStyle(0);
shape.setCurrentFillStyle1(1);
shape.setUseFillStyle1(true);
shape.processShape(new GlyphIterator(glyph, emScale, keep201Behavior));
return shape.build();
}
public int getAdvance(char c)
{
if (hmtx != null)
{
int index = getCharIndex(c);
index = cmapFmt.mapCharCode(index);
return hmtx.getAdvanceWidth(index);
}
return horizAdvanceX;
}
public int getAscent()
{
return ascent;
}
public int getDescent()
{
return descent;
}
public String getFamily()
{
return fontFamily;
}
public int getLineGap()
{
return lineGap;
}
public int getMissingGlyphCode()
{
return missingGlyphCode;
}
public int getNumGlyphs()
{
return numGlyphs;
}
public double getPointSize()
{
return 1.0f;
}
public String getPanose()
{
return panose;
}
public short getUnitsPerEm()
{
return unitsPerEm;
}
public double getEmScale()
{
return emScale;
}
public String getPostscriptName()
{
return postscriptName;
}
private CmapFormat cmapFmt = null;
private GlyfTable glyf;
private HmtxTable hmtx;
private int horizAdvanceX;
private String fontFamily;
private String subFamilyName;
private String postscriptName;
private short unitsPerEm;
private String panose;
private short ascent;
private short descent;
private short lineGap;
private boolean forceAscii;
private double emScale;
private int numGlyphs;
private short platformID;
private short encodingID;
private int usFirstCharIndex;
private int missingGlyphCode = 0;
private boolean keep201Behavior;
}
public static class GlyphIterator implements ShapeIterator
{
private final double emScale;
private double[][] segments;
private int index;
private boolean keep201Behavior;
public GlyphIterator(Glyph glyph, double emScale, boolean keep201Behavior)
{
this.emScale = emScale;
this.keep201Behavior = keep201Behavior;
if (glyph != null)
readPoints(glyph);
else
segments = new double[][]{};
// System.out.println("Batik");
// for (int i = 0; i < segments.length; i++)
// {
// System.out.println(segments[i][4] + ": " + segments[i][0] + " " + segments[i][1]);
// }
}
private void readPoints(Glyph glyph)
{
int count = glyph.getPointCount();
int offset = 0;
boolean newContour = true;
ArrayList<double[]> aSegments = new ArrayList<double[]>(count);
Point lastMove = null;
while (offset < count - 1)
{
Point point = glyph.getPoint(offset);
if (point.endOfContour)
{
newContour = true;
offset++;
continue;
}
Point point_plus1 = glyph.getPoint((offset + 1));
Point point_plus2;
//Implicit close, using the last move point as the next point
if (point_plus1.endOfContour)
point_plus2 = lastMove;
else if (offset <= count - 3)
point_plus2 = glyph.getPoint((offset + 2));
else
point_plus2 = null;
if (newContour)
{
double[] segment = new double[5];
segment[0] = point.x * emScale;
segment[1] = -point.y * emScale;
segment[4] = MOVE_TO;
aSegments.add(segment);
newContour = false;
lastMove = point;
}
else if (point.onCurve && point_plus1 != null && point_plus1.onCurve)
{
//This is a simple line
double[] segment = new double[5];
segment[0] = point_plus1.x * emScale;
segment[1] = -point_plus1.y * emScale;
segment[4] = LINE_TO;
aSegments.add(segment);
offset++;
}
else if (point.onCurve && !point_plus1.onCurve && point_plus2.onCurve)
{
// Then draw the curve, it has no implied points
double[] segment = new double[5];
segment[0] = point_plus1.x * emScale;
segment[1] = -point_plus1.y * emScale;
segment[2] = point_plus2.x * emScale;
segment[3] = -point_plus2.y * emScale;
segment[4] = QUAD_TO;
aSegments.add(segment);
//Handle implicit close situation
if (point_plus1.endOfContour)
{
offset++;
newContour = true;
}
else
{
offset += 2;
}
}
else if (point.onCurve && !point_plus1.onCurve && !point_plus2.onCurve)
{
// This is a curve with one implied point, the mid-point between the next two points
double[] segment = new double[5];
segment[0] = point_plus1.x * emScale;
segment[1] = -point_plus1.y * emScale;
segment[2] = midPoint(point_plus1.x, point_plus2.x) * emScale;
segment[3] = -midPoint(point_plus1.y, point_plus2.y) * emScale;
segment[4] = QUAD_TO;
aSegments.add(segment);
//Handle implicit close situation
if (point_plus1.endOfContour)
{
offset++;
newContour = true;
}
else
{
// !!! Laurie's change. Handle off-curve/off-curve/end. There is
// an implict curve between the second off-curve and the starting point.
// Handle implicit close situation
if (point_plus2.endOfContour && !keep201Behavior)
{
segment = new double[5];
segment[0] = point_plus2.x * emScale;
segment[1] = -point_plus2.y * emScale;
segment[2] = lastMove.x * emScale;
segment[3] = -lastMove.y * emScale;
segment[4] = QUAD_TO;
aSegments.add(segment);
// !!! END Laurie's change
}
offset += 2;
}
}
else if (!point.onCurve && !point_plus1.onCurve)
{
// This is a curve with two implied points, but the first one must have been the previous control point
double[] segment = new double[5];
segment[0] = point.x * emScale;
segment[1] = -point.y * emScale;
segment[2] = midPoint(point.x, point_plus1.x) * emScale;
segment[3] = -midPoint(point.y, point_plus1.y) * emScale;
segment[4] = QUAD_TO;
aSegments.add(segment);
// !!! Laurie's change. Handle off-curve/off-curve/end. There is
// an implict curve between the second off-curve and the starting point.
// Handle implicit close situation
if (point_plus1.endOfContour && !keep201Behavior)
{
segment = new double[5];
segment[0] = point_plus1.x * emScale;
segment[1] = -point_plus1.y * emScale;
segment[2] = lastMove.x * emScale;
segment[3] = -lastMove.y * emScale;
segment[4] = QUAD_TO;
aSegments.add(segment);
// !!! END Laurie's change
}
offset++;
}
else if (!point.onCurve && point_plus1.onCurve)
{
// This is a curve with one implied point, but it must have been the previous control point
double[] segment = new double[5];
segment[0] = point.x * emScale;
segment[1] = -point.y * emScale;
segment[2] = point_plus1.x * emScale;
segment[3] = -point_plus1.y * emScale;
segment[4] = QUAD_TO;
aSegments.add(segment);
offset++;
}
else
{
//TODO: ERROR?!
offset++;
}
}
segments = new double[aSegments.size()][];
segments = aSegments.toArray(segments);
}
private static double midPoint(int a, int b)
{
return a + (b - a) / 2.0;
}
public short currentSegment(double[] coords)
{
coords[0] = segments[index][0];
coords[1] = segments[index][1];
coords[2] = segments[index][2];
coords[3] = segments[index][3];
return (short)segments[index][4]; //TODO: A double here is a waste of memory
}
public boolean isDone()
{
return segments == null || index >= segments.length;
}
public void next()
{
index++;
}
}
public static class UnusableFontLicense extends RuntimeException
{
private static final long serialVersionUID = 1969620523936688562L;
public UnusableFontLicense(String location, String description)
{
super("The font " + location + " has a license that prevents it from being embedded: " + description + ".");
}
}
}