source: [view]
dojo.provide("dojox.gfx.VectorText");
dojo.require("dojox.gfx");
dojo.require("dojox.xml.DomParser");
dojo.require("dojox.html.metrics");
(function(){
/*
dojox.gfx.VectorText
An implementation of the SVG Font 1.1 spec, using dojox.gfx.
Basic interface:
var f = new dojox.gfx.Font(url|string);
surface||group.createVectorText(text)
.setFill(fill)
.setStroke(stroke)
.setFont(fontStyleObject);
The arguments passed to createVectorText are the same as you would
pass to surface||group.createText; the difference is that this
is entirely renderer-agnostic, and the return value is a subclass
of dojox.gfx.Group.
Note also that the "defaultText" object is slightly different:
{ type:"vectortext", x:0, y:0, width:null, height: null,
text: "", align: "start", decoration: "none" }
...as well as the "defaultVectorFont" object:
{ type:"vectorfont", size:"10pt" }
The reason for this should be obvious: most of the style for the font is defined
by the font object itself.
Note that this will only render IF and WHEN you set the font.
*/
dojo.mixin(dojox.gfx, {
vectorFontFitting: {
NONE: 0, // render text according to passed size.
FLOW: 1, // render text based on the passed width and size
FIT: 2 // render text based on a passed viewbox.
},
defaultVectorText: {
type:"vectortext", x:0, y:0, width: null, height: null,
text: "", align: "start", decoration: "none", fitting: 0, // vectorFontFitting.NONE
leading: 1.5 // in ems.
},
defaultVectorFont: {
type:"vectorfont", size: "10pt", family: null
},
_vectorFontCache: {},
_svgFontCache: {},
getVectorFont: function(/* String */url){
if(dojox.gfx._vectorFontCache[url]){
return dojox.gfx._vectorFontCache[url];
}
return new dojox.gfx.VectorFont(url);
}
});
dojo.declare("dojox.gfx.VectorFont", null, {
_entityRe: /&(quot|apos|lt|gt|amp|#x[^;]+|#\d+);/g,
_decodeEntitySequence: function(str){
// unescape the unicode sequences
// nothing to decode
if(!str.match(this._entityRe)){ return; } // undefined
var xmlEntityMap = {
amp:"&", apos:"'", quot:'"', lt:"<", gt:">"
};
// we have at least one encoded entity.
var r, tmp="";
while((r=this._entityRe.exec(str))!==null){
if(r[1].charAt(1)=="x"){
tmp += String.fromCharCode(parseInt(r[1].slice(2), 16));
}
else if(!isNaN(parseInt(r[1].slice(1),10))){
tmp += String.fromCharCode(parseInt(r[1].slice(1), 10));
}
else {
tmp += xmlEntityMap[r[1]] || "";
}
}
return tmp; // String
},
_parse: function(/* String */svg, /* String */url){
// summary:
// Take the loaded SVG Font definition file and convert the info
// into things we can use. The SVG Font definition must follow
// the SVG 1.1 Font specification.
var doc = dojox.gfx._svgFontCache[url]||dojox.xml.DomParser.parse(svg);
// font information
var f = doc.documentElement.byName("font")[0], face = doc.documentElement.byName("font-face")[0];
var unitsPerEm = parseFloat(face.getAttribute("units-per-em")||1000, 10);
var advance = {
x: parseFloat(f.getAttribute("horiz-adv-x"), 10),
y: parseFloat(f.getAttribute("vert-adv-y")||0, 10)
};
if(!advance.y){
advance.y = unitsPerEm;
}
var origin = {
horiz: {
x: parseFloat(f.getAttribute("horiz-origin-x")||0, 10),
y: parseFloat(f.getAttribute("horiz-origin-y")||0, 10)
},
vert: {
x: parseFloat(f.getAttribute("vert-origin-x")||0, 10),
y: parseFloat(f.getAttribute("vert-origin-y")||0, 10)
}
};
// face information
var family = face.getAttribute("font-family"),
style = face.getAttribute("font-style")||"all",
variant = face.getAttribute("font-variant")||"normal",
weight = face.getAttribute("font-weight")||"all",
stretch = face.getAttribute("font-stretch")||"normal",
// additional info, may not be needed
range = face.getAttribute("unicode-range")||"U+0-10FFFF",
panose = face.getAttribute("panose-1") || "0 0 0 0 0 0 0 0 0 0",
capHeight = face.getAttribute("cap-height"),
ascent = parseFloat(face.getAttribute("ascent")||(unitsPerEm-origin.vert.y), 10),
descent = parseFloat(face.getAttribute("descent")||origin.vert.y, 10),
baseline = {};
// check for font-face-src/font-face-name
var name = family;
if(face.byName("font-face-name")[0]){
name = face.byName("font-face-name")[0].getAttribute("name");
}
// see if this is cached already, and if so, forget the rest of the parsing.
if(dojox.gfx._vectorFontCache[name]){ return; }
// get any provided baseline alignment offsets.
dojo.forEach(["alphabetic", "ideographic", "mathematical", "hanging" ], function(attr){
var a = face.getAttribute(attr);
if(a !== null /* be explicit, might be 0 */){
baseline[attr] = parseFloat(a, 10);
}
});
/*
// TODO: decoration hinting.
var decoration = { };
dojo.forEach(["underline", "strikethrough", "overline"], function(type){
if(face.getAttribute(type+"-position")!=null){
decoration[type]={ };
}
});
*/
// missing glyph info
var missing = parseFloat(doc.documentElement.byName("missing-glyph")[0].getAttribute("horiz-adv-x")||advance.x, 10);
// glyph information
var glyphs = {}, glyphsByName={}, g=doc.documentElement.byName("glyph");
dojo.forEach(g, function(node){
// we are going to assume the following:
// 1) we have the unicode attribute
// 2) we have the name attribute
// 3) we have the horiz-adv-x and d attributes.
var code = node.getAttribute("unicode"),
name = node.getAttribute("glyph-name"),
xAdv = parseFloat(node.getAttribute("horiz-adv-x")||advance.x, 10),
path = node.getAttribute("d");
// unescape the unicode sequences
if(code.match(this._entityRe)){
code = this._decodeEntitySequence(code);
}
// build our glyph objects
var o = { code: code, name: name, xAdvance: xAdv, path: path };
glyphs[code]=o;
glyphsByName[name]=o;
}, this);
// now the fun part: look for kerning pairs.
var hkern=doc.documentElement.byName("hkern");
dojo.forEach(hkern, function(node, i){
var k = -parseInt(node.getAttribute("k"),10);
// look for either a code or a name
var u1=node.getAttribute("u1"),
g1=node.getAttribute("g1"),
u2=node.getAttribute("u2"),
g2=node.getAttribute("g2"),
gl;
if(u1){
// the first of the pair is a sequence of unicode characters.
// TODO: deal with unicode ranges and mulitple characters.
u1 = this._decodeEntitySequence(u1);
if(glyphs[u1]){
gl = glyphs[u1];
}
} else {
// we are referring to a name.
// TODO: deal with multiple names
if(glyphsByName[g1]){
gl = glyphsByName[g1];
}
}
if(gl){
if(!gl.kern){ gl.kern = {}; }
if(u2){
// see the notes above.
u2 = this._decodeEntitySequence(u2);
gl.kern[u2] = { x: k };
} else {
if(glyphsByName[g2]){
gl.kern[glyphsByName[g2].code] = { x: k };
}
}
}
}, this);
// pop the final definition in the font cache.
dojo.mixin(this, {
family: family,
name: name,
style: style,
variant: variant,
weight: weight,
stretch: stretch,
range: range,
viewbox: { width: unitsPerEm, height: unitsPerEm },
origin: origin,
advance: dojo.mixin(advance, {
missing:{ x: missing, y: missing }
}),
ascent: ascent,
descent: descent,
baseline: baseline,
glyphs: glyphs
});
// cache the parsed font
dojox.gfx._vectorFontCache[name] = this;
dojox.gfx._vectorFontCache[url] = this;
if(name!=family && !dojox.gfx._vectorFontCache[family]){
dojox.gfx._vectorFontCache[family] = this;
}
// cache the doc
if(!dojox.gfx._svgFontCache[url]){
dojox.gfx._svgFontCache[url]=doc;
}
},
_clean: function(){
// summary:
// Clean off all of the given mixin parameters.
var name = this.name, family = this.family;
dojo.forEach(["family","name","style","variant",
"weight","stretch","range","viewbox",
"origin","advance","ascent","descent",
"baseline","glyphs"], function(prop){
try{ delete this[prop]; } catch(e) { }
}, this);
// try to pull out of the font cache.
if(dojox.gfx._vectorFontCache[name]){
delete dojox.gfx._vectorFontCache[name];
}
if(dojox.gfx._vectorFontCache[family]){
delete dojox.gfx._vectorFontCache[family];
}
return this;
},
constructor: function(/* String|dojo._Url */url){
// summary::
// Create this font object based on the SVG Font definition at url.
this._defaultLeading = 1.5;
if(url!==undefined){
this.load(url);
}
},
load: function(/* String|dojo._Url */url){
// summary::
// Load the passed SVG and send it to the parser for parsing.
this.onLoadBegin(url.toString());
this._parse(
dojox.gfx._svgFontCache[url.toString()]||dojo._getText(url.toString()),
url.toString()
);
this.onLoad(this);
return this; // dojox.gfx.VectorFont
},
initialized: function(){
// summary::
// Return if we've loaded a font def, and the parsing was successful.
return (this.glyphs!==null); // Boolean
},
// preset round to 3 places.
_round: function(n){ return Math.round(1000*n)/1000; },
_leading: function(unit){ return this.viewbox.height * (unit||this._defaultLeading); },
_normalize: function(str){
return str.replace(/\s+/g, String.fromCharCode(0x20));
},
_getWidth: function(glyphs){
var w=0, last=0, lastGlyph=null;
dojo.forEach(glyphs, function(glyph, i){
last=glyph.xAdvance;
if(glyphs[i] && glyph.kern && glyph.kern[glyphs[i].code]){
last += glyph.kern[glyphs[i].code].x;
}
w += last;
lastGlyph = glyph;
});
// if the last glyph was a space, pull it off.
if(lastGlyph && lastGlyph.code == " "){
w -= lastGlyph.xAdvance;
}
return this._round(w/*-last*/);
},
_getLongestLine: function(lines){
var maxw=0, idx=0;
dojo.forEach(lines, function(line, i){
var max = Math.max(maxw, this._getWidth(line));
if(max > maxw){
maxw = max;
idx=i;
}
}, this);
return { width: maxw, index: idx, line: lines[idx] };
},
_trim: function(lines){
var fn = function(arr){
// check if the first or last character is a space and if so, remove it.
if(!arr.length){ return; }
if(arr[arr.length-1].code == " "){ arr.splice(arr.length-1, 1); }
if(!arr.length){ return; }
if(arr[0].code == " "){ arr.splice(0, 1); }
};
if(dojo.isArray(lines[0])){
// more than one line.
dojo.forEach(lines, fn);
} else {
fn(lines);
}
return lines;
},
_split: function(chars, nLines){
// summary:
// split passed chars into nLines by finding the closest whitespace.
var w = this._getWidth(chars),
limit = Math.floor(w/nLines),
lines = [],
cw = 0,
c = [],
found = false;
for(var i=0, l=chars.length; i if(chars[i].code == " "){ found = true; }
cw += chars[i].xAdvance;
if(i+1 cw += chars[i].kern[chars[i+1].code].x;
}
if(cw>=limit){
var chr=chars[i];
while(found && chr.code != " " && i>=0){
chr = c.pop(); i--;
}
lines.push(c);
c=[];
cw=0;
found=false;
}
c.push(chars[i]);
}
if(c.length){ lines.push(c); }
// "trim" it
return this._trim(lines);
},
_getSizeFactor: function(size){
// given the size, return a scaling factor based on the height of the
// font as defined in the font definition file.
size += ""; // force the string cast.
var metrics = dojox.html.metrics.getCachedFontMeasurements(),
height=this.viewbox.height,
f=metrics["1em"],
unit=parseFloat(size, 10); // the default.
if(size.indexOf("em")>-1){
return this._round((metrics["1em"]*unit)/height);
}
else if(size.indexOf("ex")>-1){
return this._round((metrics["1ex"]*unit)/height);
}
else if(size.indexOf("pt")>-1){
return this._round(((metrics["12pt"] / 12)*unit) / height);
}
else if(size.indexOf("px")>-1){
return this._round(((metrics["16px"] / 16)*unit) / height);
}
else if(size.indexOf("%")>-1){
return this._round((metrics["1em"]*(unit / 100)) / height);
}
else {
f=metrics[size]||metrics.medium;
return this._round(f/height);
}