/*

   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 org.apache.batik.svggen;

import java.awt.Font;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphMetrics;
import java.awt.font.GlyphVector;
import java.awt.font.LineMetrics;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.util.HashMap;
import java.util.Map;

import org.apache.batik.ext.awt.g2d.GraphicContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

Utility class that converts a Font object into a set of SVG font attributes
Author:Christophe Jolif, Vincent Hardy
Version:$Id: SVGFont.java 1802297 2017-07-18 13:58:12Z ssteiner $
/** * Utility class that converts a Font object into a set of SVG * font attributes * * @author <a href="mailto:cjolif@ilog.fr">Christophe Jolif</a> * @author <a href="mailto:vincent.hardy@eng.sun.com">Vincent Hardy</a> * @version $Id: SVGFont.java 1802297 2017-07-18 13:58:12Z ssteiner $ */
public class SVGFont extends AbstractSVGConverter { public static final float EXTRA_LIGHT = TextAttribute.WEIGHT_EXTRA_LIGHT; public static final float LIGHT = TextAttribute.WEIGHT_LIGHT; public static final float DEMILIGHT = TextAttribute.WEIGHT_DEMILIGHT; public static final float REGULAR = TextAttribute.WEIGHT_REGULAR; public static final float SEMIBOLD = TextAttribute.WEIGHT_SEMIBOLD; public static final float MEDIUM = TextAttribute.WEIGHT_MEDIUM; public static final float DEMIBOLD = TextAttribute.WEIGHT_DEMIBOLD; public static final float BOLD = TextAttribute.WEIGHT_BOLD; public static final float HEAVY = TextAttribute.WEIGHT_HEAVY; public static final float EXTRABOLD = TextAttribute.WEIGHT_EXTRABOLD; public static final float ULTRABOLD = TextAttribute.WEIGHT_ULTRABOLD; public static final float POSTURE_REGULAR = TextAttribute.POSTURE_REGULAR; public static final float POSTURE_OBLIQUE = TextAttribute.POSTURE_OBLIQUE;
Contains threshold value for the various Font styles. If a given style is in an interval, then it is mapped to the style at the top of that interval.
See Also:
  • styleToSVG
/** * Contains threshold value for the various Font styles. If a given * style is in an interval, then it is mapped to the style at the top * of that interval. * @see #styleToSVG */
static final float[] fontStyles = { POSTURE_REGULAR + (POSTURE_OBLIQUE - POSTURE_REGULAR)/2 };
SVG Styles corresponding to the fontStyles
/** * SVG Styles corresponding to the fontStyles */
static final String[] svgStyles = { /*POSTURE_REGULAR*/ SVG_NORMAL_VALUE, /*POSTURE_OBLIQUE*/ SVG_ITALIC_VALUE };
Contains threshold values for the various Font weights. If a given weight is in an interval, then it is mapped to the weight at the top of the interval.
See Also:
  • weightToSVG
/** * Contains threshold values for the various Font weights. If a given * weight is in an interval, then it is mapped to the weight at the top * of the interval. * @see #weightToSVG */
static final float[] fontWeights = { EXTRA_LIGHT + (LIGHT - EXTRA_LIGHT)/2f, LIGHT + (DEMILIGHT - LIGHT)/2f, DEMILIGHT + (REGULAR - DEMILIGHT)/2f, REGULAR + (SEMIBOLD - REGULAR)/2f, SEMIBOLD + (MEDIUM - SEMIBOLD)/2f, MEDIUM + (DEMIBOLD - MEDIUM)/2f, DEMIBOLD + (BOLD - DEMIBOLD)/2f, BOLD + (HEAVY - BOLD)/2f, HEAVY + (EXTRABOLD - HEAVY)/2f, EXTRABOLD + (ULTRABOLD - EXTRABOLD), };
SVG Weights corresponding to the fontWeights
/** * SVG Weights corresponding to the fontWeights */
static final String[] svgWeights = { /*EXTRA_LIGHT*/ SVG_100_VALUE, /*LIGHT*/ SVG_200_VALUE, /*DEMILIGHT*/ SVG_300_VALUE, /*REGULAR*/ SVG_NORMAL_VALUE, /*SEMIBOLD*/ SVG_500_VALUE, /*MEDIUM*/ SVG_500_VALUE, /*DEMIBOLD*/ SVG_600_VALUE, /*BOLD*/ SVG_BOLD_VALUE, /*HEAVY*/ SVG_800_VALUE, /*EXTRABOLD*/ SVG_800_VALUE, /*ULTRABOLD*/ SVG_900_VALUE };
Logical fonts mapping
/** * Logical fonts mapping */
static Map logicalFontMap = new HashMap(); static { logicalFontMap.put("dialog", "sans-serif"); logicalFontMap.put("dialoginput", "monospace"); logicalFontMap.put("monospaced", "monospace"); logicalFontMap.put("serif", "serif"); logicalFontMap.put("sansserif", "sans-serif"); logicalFontMap.put("symbol", "'WingDings'"); }
The common font size to use when generating all SVG fonts.
/** * The common font size to use when generating all SVG fonts. */
static final int COMMON_FONT_SIZE = 100;
Used to keep track of which characters have been rendered by each font used. MapKey is the fontKey, mapValue is a sorted array of used characters.
/** * Used to keep track of which characters have been rendered by each font * used. MapKey is the fontKey, mapValue is a sorted array of used characters. */
final Map fontStringMap = new HashMap();
Params:
  • generatorContext – used to build Elements
/** * @param generatorContext used to build Elements */
public SVGFont(SVGGeneratorContext generatorContext) { super(generatorContext); }
Records that the specified font has been used to draw the text string. This is so we can keep track of which glyphs are required for each SVG font that is generated.
/** * Records that the specified font has been used to draw the text string. * This is so we can keep track of which glyphs are required for each * SVG font that is generated. */
public void recordFontUsage(String string, Font font) { Font commonSizeFont = createCommonSizeFont(font); String fontKey = (commonSizeFont.getFamily() + commonSizeFont.getStyle()); // String textUsingFont = (String)fontStringMap.get(fontKey); // if (textUsingFont == null) { // // font has not been used before // textUsingFont = ""; // } // // // append any new characters to textUsingFont // // FIXX: This is horribly inefficent, consider binary tree, Set, etc. // for (int i = 0; i < string.length(); i++) { // char ch = string.charAt(i); // if (textUsingFont.indexOf(ch) == -1) { // textUsingFont += ch; // } // } CharListHelper chl = (CharListHelper) fontStringMap.get( fontKey ); if ( chl == null ){ // was not in use before, so we need to create a fresh one chl = new CharListHelper(); } for (int i = 0; i < string.length(); i++) { char ch = string.charAt(i); // todo take care of surrogate chars here... chl.add( ch ); } fontStringMap.put(fontKey, chl ); }
Creates a new Font that is of the common font size used for generating SVG fonts. The new Font will be the same as the specified font, with only its size attribute modified.
/** * Creates a new Font that is of the common font size used for generating * SVG fonts. The new Font will be the same as the specified font, with * only its size attribute modified. */
private static Font createCommonSizeFont(Font font) { Map attributes = new HashMap(); attributes.put(TextAttribute.SIZE, (float) COMMON_FONT_SIZE); // Remove Transform from font otherwise it will be applied twice. attributes.put(TextAttribute.TRANSFORM, null); return font.deriveFont(attributes); }
Converts part or all of the input GraphicContext into a set of attribute/value pairs and related definitions
Params:
  • gc – GraphicContext to be converted
See Also:
Returns:descriptor of the attributes required to represent some or all of the GraphicContext state, along with the related definitions
/** * Converts part or all of the input GraphicContext into * a set of attribute/value pairs and related definitions * * @param gc GraphicContext to be converted * @return descriptor of the attributes required to represent * some or all of the GraphicContext state, along * with the related definitions * @see org.apache.batik.svggen.SVGDescriptor */
public SVGDescriptor toSVG(GraphicContext gc) { return toSVG(gc.getFont(), gc.getFontRenderContext()); }
Params:
  • font – Font object which should be converted to a set of SVG attributes
  • frc – The FontRenderContext which will be used to generate glyph elements for the SVGFont definition element
Returns:description of attribute values that describe the font
/** * @param font Font object which should be converted to a set * of SVG attributes * @param frc The FontRenderContext which will be used to generate glyph * elements for the SVGFont definition element * @return description of attribute values that describe the font */
public SVGFontDescriptor toSVG(Font font, FontRenderContext frc) { // Remove affine from FRC otherwise it will be applied twice. FontRenderContext localFRC; localFRC = new FontRenderContext(new AffineTransform(), frc.isAntiAliased(), frc.usesFractionalMetrics()); String fontSize = doubleString(font.getSize2D()) + "px"; String fontWeight = weightToSVG(font); String fontStyle = styleToSVG(font); String fontFamilyStr = familyToSVG(font); Font commonSizeFont = createCommonSizeFont(font); String fontKey = (commonSizeFont.getFamily() + commonSizeFont.getStyle()); CharListHelper clh = (CharListHelper)fontStringMap.get(fontKey); if (clh == null) { // this font hasn't been used by any text yet, // so don't create an SVG Font element for it return new SVGFontDescriptor(fontSize, fontWeight, fontStyle, fontFamilyStr, null); } Document domFactory = generatorContext.domFactory; // see if a description already exists for this font SVGFontDescriptor fontDesc = (SVGFontDescriptor)descMap.get(fontKey); Element fontDef; if (fontDesc != null) { // use the SVG Font element that has already been created fontDef = fontDesc.getDef(); } else { // create a new SVG Font element fontDef = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_FONT_TAG); // // create the font-face element // Element fontFace = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_FONT_FACE_TAG); String svgFontFamilyString = fontFamilyStr; if (fontFamilyStr.startsWith("'") && fontFamilyStr.endsWith("'")) { // get rid of the quotes svgFontFamilyString = fontFamilyStr.substring(1, fontFamilyStr.length()-1); } fontFace.setAttributeNS(null, SVG_FONT_FAMILY_ATTRIBUTE, svgFontFamilyString); fontFace.setAttributeNS(null, SVG_FONT_WEIGHT_ATTRIBUTE, fontWeight); fontFace.setAttributeNS(null, SVG_FONT_STYLE_ATTRIBUTE, fontStyle); fontFace.setAttributeNS(null, SVG_UNITS_PER_EM_ATTRIBUTE, ""+COMMON_FONT_SIZE); fontDef.appendChild(fontFace); // // create missing glyph element // Element missingGlyphElement = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_MISSING_GLYPH_TAG); int[] missingGlyphCode = new int[1]; missingGlyphCode[0] = commonSizeFont.getMissingGlyphCode(); GlyphVector gv; gv = commonSizeFont.createGlyphVector(localFRC, missingGlyphCode); Shape missingGlyphShape = gv.getGlyphOutline(0); GlyphMetrics gm = gv.getGlyphMetrics(0); // need to turn the missing glyph upside down to be in the font // coordinate system (i.e Y axis up) AffineTransform at = AffineTransform.getScaleInstance(1, -1); missingGlyphShape = at.createTransformedShape(missingGlyphShape); missingGlyphElement.setAttributeNS(null, SVG_D_ATTRIBUTE, SVGPath.toSVGPathData(missingGlyphShape, generatorContext)); missingGlyphElement.setAttributeNS(null, SVG_HORIZ_ADV_X_ATTRIBUTE, String.valueOf( gm.getAdvance() ) ); fontDef.appendChild(missingGlyphElement); // set the font's default horizontal advance to be the same as // the missing glyph fontDef.setAttributeNS(null, SVG_HORIZ_ADV_X_ATTRIBUTE, String.valueOf( gm.getAdvance() ) ); // set the ascent and descent attributes LineMetrics lm = commonSizeFont.getLineMetrics("By", localFRC); fontFace.setAttributeNS(null, SVG_ASCENT_ATTRIBUTE, String.valueOf( lm.getAscent() ) ); fontFace.setAttributeNS(null, SVG_DESCENT_ATTRIBUTE, String.valueOf( lm.getDescent() ) ); // // Font ID // fontDef.setAttributeNS(null, SVG_ID_ATTRIBUTE, generatorContext.idGenerator.generateID(ID_PREFIX_FONT)); } // // add any new glyphs to the fontDef here // String textUsingFont = clh.getNewChars(); clh.clearNewChars(); // process the characters in textUsingFont backwards since the new chars // are at the end, can stop when find a char that already has a glyph for (int i = textUsingFont.length()-1; i >= 0; i--) { char c = textUsingFont.charAt(i); String searchStr = String.valueOf( c ); boolean foundGlyph = false; NodeList fontChildren = fontDef.getChildNodes(); for (int j = 0; j < fontChildren.getLength(); j++) { if (fontChildren.item(j) instanceof Element) { Element childElement = (Element)fontChildren.item(j); if (childElement.getAttributeNS(null, SVG_UNICODE_ATTRIBUTE).equals( searchStr )) { foundGlyph = true; break; } } } if (!foundGlyph) { // need to create one Element glyphElement = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_GLYPH_TAG); GlyphVector gv; gv = commonSizeFont.createGlyphVector(localFRC, ""+c); Shape glyphShape = gv.getGlyphOutline(0); GlyphMetrics gm = gv.getGlyphMetrics(0); // need to turn the glyph upside down to be in the font // coordinate system (i.e Y axis up) AffineTransform at = AffineTransform.getScaleInstance(1, -1); glyphShape = at.createTransformedShape(glyphShape); glyphElement.setAttributeNS(null, SVG_D_ATTRIBUTE, SVGPath.toSVGPathData(glyphShape, generatorContext)); glyphElement.setAttributeNS(null, SVG_HORIZ_ADV_X_ATTRIBUTE, String.valueOf( gm.getAdvance() ) ); glyphElement.setAttributeNS(null, SVG_UNICODE_ATTRIBUTE, String.valueOf( c ) ); fontDef.appendChild(glyphElement); } else { // have reached the chars in textUsingFont that already // have glyphs, don't need to process any further break; } } // // create a new font description for this instance of the font usage // SVGFontDescriptor newFontDesc = new SVGFontDescriptor(fontSize, fontWeight, fontStyle, fontFamilyStr, fontDef); // // Update maps so that the font def can be reused if needed // if (fontDesc == null) { descMap.put(fontKey, newFontDesc); defSet.add(fontDef); } return newFontDesc; }
Params:
  • font – whose family should be converted to an SVG string value.
/** * @param font whose family should be converted to an SVG string * value. */
public static String familyToSVG(Font font) { String fontFamilyStr = font.getFamily(); String logicalFontFamily = (String)logicalFontMap.get(font.getName().toLowerCase()); if (logicalFontFamily != null) fontFamilyStr = logicalFontFamily; else { final char QUOTE = '\''; fontFamilyStr = QUOTE + fontFamilyStr + QUOTE; } return fontFamilyStr; }
Params:
  • font – whose style should be converted to an SVG string value.
/** * @param font whose style should be converted to an SVG string * value. */
public static String styleToSVG(Font font) { Map attrMap = font.getAttributes(); Float styleValue = (Float)attrMap.get(TextAttribute.POSTURE); if (styleValue == null) { if (font.isItalic()) styleValue = TextAttribute.POSTURE_OBLIQUE; else styleValue = TextAttribute.POSTURE_REGULAR; } float style = styleValue; int i = 0; for (i=0; i< fontStyles.length; i++) { if (style <= fontStyles[i]) break; } return svgStyles[i]; }
Params:
  • font – whose weight should be converted to an SVG string value. Note that there is loss of precision for semibold and extrabold.
/** * @param font whose weight should be converted to an SVG string * value. Note that there is loss of precision for * semibold and extrabold. */
public static String weightToSVG(Font font) { Map attrMap = font.getAttributes(); Float weightValue = (Float)attrMap.get(TextAttribute.WEIGHT); if (weightValue==null) { if (font.isBold()) weightValue = TextAttribute.WEIGHT_BOLD; else weightValue = TextAttribute.WEIGHT_REGULAR; } float weight = weightValue; int i = 0; for (i=0; i<fontWeights.length; i++) { if (weight<=fontWeights[i]) break; } return svgWeights[i]; }
this helper-class implements a set of characters. it stores all used characters in a font.
implementation: we keep a sorted list of integers. This allows to use binary search for lookup and insert. The use of int instead of char allows us to handle surrogate characters as well.
/** * this helper-class implements a set of characters. it stores all used characters * in a font. * * <br>implementation: we keep a sorted list of integers. This allows to use binary search * for lookup and insert. The use of <code>int</code> instead of <code>char</code> allows us to * handle surrogate characters as well. */
private static class CharListHelper {
the number of slots actually used. must always be 0 <= nUsed <= charList.length
/** * the number of slots actually used. * must always be 0 &lt;= nUsed &lt;= charList.length */
private int nUsed = 0;
keeps added characters, is kept sorted for efficient search.
/** * keeps added characters, is kept sorted for efficient search. */
private int[] charList = new int[ 40 ];
this keeps all added characters in order. It can be cleared from toSVG() when glyphs are created for some characters.
/** * this keeps all added characters in order. It can be cleared from toSVG() * when glyphs are created for some characters. */
private StringBuffer freshChars = new StringBuffer( 40 ); CharListHelper() { }
get a string of all characters added since last call to clearNewChars().
Returns:a string of all recently added characters
/** * get a string of all characters added since last call to clearNewChars(). * @return a string of all recently added characters */
String getNewChars(){ return freshChars.toString(); }
reset the string of recently added characters - used after glyphs were created for them.
/** * reset the string of recently added characters - used after glyphs were created for them. */
void clearNewChars(){ freshChars = new StringBuffer( 40 ); }
test, if the character is contained in the charList. If not, insert c into charList. charList is kept sorted for efficient search.
Params:
  • c –
Returns:true, when fresh inserted
/** * test, if the character is contained in the charList. * If not, insert c into charList. * charList is kept sorted for efficient search. * @param c * @return true, when fresh inserted */
boolean add( int c ){ int pos = binSearch( charList, nUsed, c ); if ( pos >= 0 ){ // was in list: no activity needed return false; } // insert new char into array, grow if necessary if ( nUsed == charList.length ){ // full, allocate some more slots - moderately... int[] t = new int[ nUsed + 20 ]; System.arraycopy( charList, 0, t, 0, nUsed ); charList = t; } // now we can insert the new character pos = -pos -1; System.arraycopy( charList, pos, charList, pos+1, nUsed - pos ); charList[ pos ] = c; freshChars.append( (char)c ); // todo if necessary split surrogates here nUsed++; return true; }
unfortunatly, Arrays.binarySearch() does not support search in a part of the array (not in jdk1.3 and jdk1.4). - so we have do provide our own implementation.
Params:
  • list – to search within
  • nUsed – the last used index, can be < list.length
  • chr – the character to lookup
Returns:the index when found, or the negative insert position.
/** * unfortunatly, Arrays.binarySearch() does not support search in a * part of the array (not in jdk1.3 and jdk1.4). - so we have do provide our own * implementation. * @param list to search within * @param nUsed the last used index, can be &lt; list.length * @param chr the character to lookup * @return the index when found, or the negative insert position. */
static int binSearch( int[] list, int nUsed, int chr ){ int low = 0; int high = nUsed -1; while ( low <= high ) { int mid = ( low + high ) >>> 1; // we're not sun - we know how to binSearch... int midVal = list[ mid ]; if ( midVal < chr ) { low = mid + 1; } else if ( midVal > chr ) { high = mid - 1; } else { return mid; // char found } } return -( low + 1 ); // char not found, should be inserted at -pos -1 } } }