1 /* $Id$ */
  2 
  3 /** 
  4  * @projectDescription An cross-browser implementation of the HTML5 <canvas> text methods
  5  * @author Fabien M�nager
  6  * @version $Revision$
  7  * @license MIT License <http://www.opensource.org/licenses/mit-license.php>
  8  */
  9 
 10 /**
 11  * Known issues:
 12  * - The 'light' font weight is not supported, neither is the 'oblique' font style.
 13  * - Optimize the different hacks (for Opera9)
 14  */
 15 
 16 window.Canvas = window.Canvas || {};
 17 window.Canvas.Text = {
 18   // http://mondaybynoon.com/2007/04/02/linux-font-equivalents-to-popular-web-typefaces/
 19   equivalentFaces: {
 20     // Web popular fonts
 21     'arial': ['liberation sans', 'nimbus sans l', 'freesans'],
 22     'times new roman': ['liberation serif', 'linux libertine', 'freeserif'],
 23     'courier new': ['dejavu sans mono', 'liberation mono', 'nimbus mono l', 'freemono'],
 24     'georgia': ['nimbus roman no9 l'],
 25     'helvetica': ['nimbus sans l', 'freesans'],
 26     'tahoma': ['dejavu sans', 'bitstream vera sans'],
 27     'verdana': ['dejavu sans', 'bitstream vera sans']
 28   },
 29   genericFaces: {
 30     'serif': ['times new roman', 'georgia', 'garamond', 'bodoni', 'minion web', 'itc stone serif', 'bitstream cyberbit'],
 31     'sans-serif': ['arial', 'verdana', 'trebuchet', 'tahoma', 'helvetica', 'itc avant garde gothic', 'univers', 'futura', 
 32                    'gill sans', 'akzidenz grotesk', 'attika', 'typiko new era', 'itc stone sans', 'monotype gill sans 571'],
 33     'monospace': ['courier', 'courier new', 'prestige', 'everson mono'],
 34     'cursive': ['caflisch script', 'adobe poetica', 'sanvito', 'ex ponto', 'snell roundhand', 'zapf-chancery'],
 35     'fantasy': ['alpha geometrique', 'critter', 'cottonwood', 'fb reactor', 'studz']
 36   },
 37   faces: {},
 38   scaling: 0.962,
 39   _styleCache: {}
 40 };
 41 
 42 /** The implementation of the text functions */
 43 (function(){
 44   var isOpera9 = (window.opera && navigator.userAgent.match(/Opera\/9/)), // It seems to be faster when the hacked methods are used. But there are artifacts with Opera 10.
 45       proto = window.CanvasRenderingContext2D ? window.CanvasRenderingContext2D.prototype : document.createElement('canvas').getContext('2d').__proto__,
 46       ctxt = window.Canvas.Text;
 47 
 48   // Global options
 49   ctxt.options = {
 50     fallbackCharacter: ' ', // The character that will be drawn when not present in the font face file
 51     dontUseMoz: false, // Don't use the builtin Firefox 3.0 functions (mozDrawText, mozPathText and mozMeasureText)
 52     reimplement: false, // Don't use the builtin official functions present in Chrome 2, Safari 4, and Firefox 3.1+
 53     debug: false, // Debug mode, not used yet
 54     autoload: 'faces'
 55   };
 56   
 57   function initialize(){
 58     var libFileName = 'canvas.text.js',
 59         scripts = document.getElementsByTagName("script"), i, j;
 60 
 61     for (i = 0; i < scripts.length; i++) {
 62       var src = scripts[i].src;
 63       if (src.indexOf(libFileName) != -1) {
 64         var parts = src.split('?');
 65         ctxt.basePath = parts[0].replace(libFileName, '');
 66         if (parts[1]) {
 67           var options = parts[1].split('&');
 68           for (j = options.length-1; j >= 0; --j) {
 69             var pair = options[j].split('=');
 70             ctxt.options[pair[0]] = pair[1];
 71           }
 72         }
 73         break;
 74       }
 75     }
 76   }
 77   initialize();
 78   
 79   // What is the browser's implementation ?
 80   var moz = !ctxt.options.dontUseMoz && proto.mozDrawText && !proto.strokeText;
 81 
 82   // If the text functions are already here : nothing to do !
 83   if (proto.strokeText && !ctxt.options.reimplement) {
 84     // This property is needed, when including the font face files
 85     return window._typeface_js = {loadFace: function(){}};
 86   }
 87   
 88   function getCSSWeightEquivalent(weight){
 89     switch(weight) {
 90       case 'bolder':
 91       case 'bold':
 92       case '900':
 93       case '800':
 94       case '700': return 'bold';
 95       case '600':
 96       case '500':
 97       case '400':
 98       default:
 99       case 'normal': return 'normal';
100       //default: return 'light';
101     }
102   }
103   
104   function getElementStyle(e){
105     if (document.defaultView && document.defaultView.getComputedStyle) {
106       return document.defaultView.getComputedStyle(e, null);
107     } else if (e.currentStyle) {
108       return e.currentStyle;
109     } else {
110       return e.style;
111     }
112   }
113   
114   function getXHR(){
115     if (!ctxt.xhr) {
116       var methods = [
117         function(){return new XMLHttpRequest()},
118         function(){return new ActiveXObject('Msxml2.XMLHTTP')},
119         function(){return new ActiveXObject('Microsoft.XMLHTTP')}
120       ];
121       for (i = 0; i < methods.length; i++) {
122         try {
123           ctxt.xhr = methods[i](); 
124           break;
125         } 
126         catch (e) {}
127       }
128     }
129     return ctxt.xhr;
130   }
131 
132   function arrayContains(a, v){
133     var i, l = a.length;
134     for (i = l-1; i >= 0; --i) if (a[i] === v) return true;
135     return false;
136   }
137   
138   ctxt.lookupFamily = function(family){
139     var faces = this.faces, face, i, f, list,
140         equiv = this.equivalentFaces,
141         generic = this.genericFaces;
142         
143     if (faces[family]) return faces[family];
144     
145     if (generic[family]) {
146       for (i = 0; i < generic[family].length; i++) {
147         if (f = this.lookupFamily(generic[family][i])) return f;
148       }
149     }
150     
151     if (!(list = equiv[family])) return false;
152 
153     for (i = 0; i < list.length; i++)
154       if (face = faces[list[i]]) return face;
155     return false;
156   }
157 
158   ctxt.getFace = function(family, weight, style){
159     var face = this.lookupFamily(family);
160     if (!face) return false;
161     
162     if (face && 
163         face[weight] && 
164         face[weight][style]) return face[weight][style];
165     
166     if (!this.options.autoload) return false;
167     
168     var faceName = (family.replace(/[ -]/g, '_')+'-'+weight+'-'+style),
169         xhr = this.xhr,
170         url = this.basePath+this.options.autoload+'/'+faceName+'.js';
171 
172     xhr = getXHR();
173     xhr.open("get", url, false);
174     xhr.send(null);
175     if(xhr.status == 200) {
176       eval(xhr.responseText);
177       return this.faces[family][weight][style];
178     }
179     else throw 'Unable to load the font ['+family+' '+weight+' '+style+']';
180     return false;
181   };
182   
183   ctxt.loadFace = function(data){
184     var family = data.familyName.toLowerCase();
185     this.faces[family] = this.faces[family] || {};
186     this.faces[family][data.cssFontWeight] = this.faces[family][data.cssFontWeight] || {};
187     this.faces[family][data.cssFontWeight][data.cssFontStyle] = data;
188     return data;
189   };
190 	
191   // To use the typeface.js face files
192   window._typeface_js = {faces: ctxt.faces, loadFace: ctxt.loadFace};
193   
194   ctxt.getFaceFromStyle = function(style){
195     var weight = getCSSWeightEquivalent(style.weight),
196         families = style.family, i, face;
197         
198     for (i = 0; i < families.length; i++) {
199       if (face = this.getFace(families[i].toLowerCase(), weight, style.style)) {
200         return face;
201       }
202     }
203     return false;
204   };
205   
206   // Default values
207   // Firefox 3.5 throws an error when redefining these properties
208   try {
209     proto.font = "10px sans-serif";
210     proto.textAlign = "start";
211     proto.textBaseline = "alphabetic";
212   }
213   catch(e){}
214   
215   proto.parseStyle = function(styleText){
216     if (ctxt._styleCache[styleText]) return this.getComputedStyle(ctxt._styleCache[styleText]);
217     
218     var style = {}, computedStyle, families;
219     
220     if (!this._elt) {
221       this._elt = document.createElement('span');
222       this.canvas.appendChild(this._elt);
223     }
224     
225     // Default style
226     this.canvas.font = '10px sans-serif';
227     this._elt.style.font = styleText;
228     
229     computedStyle = getElementStyle(this._elt);
230     style.size = computedStyle.fontSize;
231     style.weight = computedStyle.fontWeight;
232     style.style = computedStyle.fontStyle;
233     
234     families = computedStyle.fontFamily.split(',');
235     for(i = 0; i < families.length; i++) {
236       families[i] = families[i].replace(/^["'\s]*/, '').replace(/["'\s]*$/, '');
237     }
238     style.family = families;
239     
240     return this.getComputedStyle(ctxt._styleCache[styleText] = style);
241   };
242   
243   proto.buildStyle = function (style){
244     return style.style+' '+style.weight+' '+style.size+'px "'+style.family+'"';
245   };
246 
247   proto.renderText = function(text, style){
248     var face = ctxt.getFaceFromStyle(style),
249         scale = (style.size / face.resolution) * (3/4),
250         offset = 0, i, 
251 				chars = text.split(''), 
252 				length = chars.length;
253     
254     if (!isOpera9) {
255       this.scale(scale, -scale);
256       this.lineWidth /= scale;
257     }
258     
259     for (i = 0; i < length; i++) {
260       offset += this.renderGlyph(chars[i], face, scale, offset);
261     }
262   };
263 
264   if (isOpera9) {
265     proto.renderGlyph = function(c, face, scale, offset){
266       var i, cpx, cpy, outline, action, glyph = face.glyphs[c], length;
267       
268       if (!glyph) return;
269   
270       if (glyph.o) {
271         outline = glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(' '));
272         length = outline.length;
273         for (i = 0; i < length; ) {
274           action = outline[i++];
275   
276           switch(action) {
277             case 'm':
278               this.moveTo(outline[i++]*scale+offset, outline[i++]*-scale);
279               break;
280             case 'l':
281               this.lineTo(outline[i++]*scale+offset, outline[i++]*-scale);
282               break;
283             case 'q':
284               cpx = outline[i++]*scale+offset;
285               cpy = outline[i++]*-scale;
286               this.quadraticCurveTo(outline[i++]*scale+offset, outline[i++]*-scale, cpx, cpy);
287               break;
288           }
289         }
290       }
291       return glyph.ha * scale;
292     };
293   }
294   else {
295     proto.renderGlyph = function(c, face){
296       var i, cpx, cpy, outline, action, glyph = face.glyphs[c], length;
297       
298       if (!glyph) return;
299 
300       if (glyph.o) {
301         outline = glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(' '));
302         length = outline.length;
303         for (i = 0; i < length; ) {
304           action = outline[i++];
305  
306           switch(action) {
307             case 'm':
308               this.moveTo(outline[i++], outline[i++]);
309               break;
310             case 'l':
311               this.lineTo(outline[i++], outline[i++]);
312               break;
313             case 'q':
314               cpx = outline[i++];
315               cpy = outline[i++];
316               this.quadraticCurveTo(outline[i++], outline[i++], cpx, cpy);
317               break;
318           }
319         }
320       }
321       if (glyph.ha) this.translate(glyph.ha, 0);
322     };
323   }
324   
325   proto.getTextExtents = function(text, style){
326     var width = 0, height = 0, ha = 0, 
327         face = ctxt.getFaceFromStyle(style),
328         i, glyph;
329     
330     for (i = 0; i < text.length; i++) {
331       glyph = face.glyphs[text.charAt(i)] || face.glyphs[ctxt.options.fallbackCharacter];
332       width += Math.max(glyph.ha, glyph.x_max);
333       ha += glyph.ha;
334     }
335     
336     return {
337       width: width,
338       height: face.lineHeight,
339       ha: ha
340     };
341   };
342   
343   proto.getComputedStyle = function(style){
344     var p, canvasStyle = getElementStyle(this.canvas), 
345         computedStyle = {},
346 				s = style.size,
347         canvasFontSize = parseFloat(canvasStyle.fontSize),
348         fontSize = parseFloat(s);
349     
350     for (p in style) {
351       computedStyle[p] = style[p];
352     }
353     
354     // Compute the size
355     if (typeof s == 'number' || s.indexOf('px') != -1) 
356       computedStyle.size = fontSize;
357     else if (s.indexOf('em') != -1)
358       computedStyle.size = canvasFontSize * fontSize;
359     else if (s.indexOf('%') != -1)
360       computedStyle.size = (canvasFontSize / 100) * fontSize;
361     else if (s.indexOf('pt') != -1)
362       computedStyle.size = canvasFontSize * (4/3) * fontSize;
363     else
364       computedStyle.size = canvasFontSize;
365     
366     return computedStyle;
367   };
368   
369   proto.getTextOffset = function(text, style, face){
370     var canvasStyle = getElementStyle(this.canvas),
371         metrics = this.measureText(text), 
372         scale = (style.size / face.resolution) * (3/4),
373         offset = {x: 0, y: 0, metrics: metrics, scale: scale};
374 
375     switch (this.textAlign) {
376       default:
377       case null:
378       case 'left': break;
379       case 'center': offset.x = -metrics.width/2; break;
380       case 'right':  offset.x = -metrics.width; break;
381       case 'start':  offset.x = (canvasStyle.direction == 'rtl') ? -metrics.width : 0; break;
382       case 'end':    offset.x = (canvasStyle.direction == 'ltr') ? -metrics.width : 0; break;
383     }
384     
385     switch (this.textBaseline) {
386       case 'alphabetic': break;
387       default:
388       case null:
389       case 'ideographic':
390       case 'bottom': offset.y = face.descender; break;
391       case 'hanging': 
392       case 'top': offset.y = face.ascender; break;
393       case 'middle': offset.y = (face.ascender + face.descender) / 2; break;
394     }
395     offset.y *= scale;
396     return offset;
397   };
398 
399   proto.drawText = function(text, x, y, maxWidth, stroke){
400     var style = this.parseStyle(this.font),
401         face = ctxt.getFaceFromStyle(style),
402         offset = this.getTextOffset(text, style, face);
403     
404     this.save();
405     this.translate(x + offset.x, y + offset.y);
406     if (face.strokeFont && !stroke) {
407       this.strokeStyle = this.fillStyle;
408     }
409     this.beginPath();
410 
411     if (moz) {
412       this.mozTextStyle = this.buildStyle(style);
413       this[stroke ? 'mozPathText' : 'mozDrawText'](text);
414     }
415     else {
416       this.scale(ctxt.scaling, ctxt.scaling);
417       this.renderText(text, style);
418       if (face.strokeFont) {
419         this.lineWidth = style.size * (style.weight == 'bold' ? 0.5 : 0.3);
420       }
421     }
422 
423     this[(stroke || (face.strokeFont && !moz)) ? 'stroke' : 'fill']();
424 
425     this.closePath();
426     this.restore();
427     
428     if (ctxt.options.debug) {
429       var left = Math.floor(offset.x + x) + 0.5,
430           top = Math.floor(y)+0.5;
431           
432       this.save();
433       this.strokeStyle = '#F00';
434       this.lineWidth = 0.5;
435       this.beginPath();
436       
437       // Text baseline
438       this.moveTo(left + offset.metrics.width, top);
439       this.lineTo(left, top);
440       
441       // Text align
442       this.moveTo(left - offset.x, top + offset.y);
443       this.lineTo(left - offset.x, top + offset.y - style.size);
444       
445       this.stroke();
446       this.closePath();
447       this.restore();
448     }
449   };
450   
451   proto.fillText = function(text, x, y, maxWidth){
452     this.drawText(text, x, y, maxWidth, false);
453   };
454   
455   proto.strokeText = function(text, x, y, maxWidth){
456     this.drawText(text, x, y, maxWidth, true);
457   };
458   
459   proto.measureText = function(text){
460     var style = this.parseStyle(this.font), 
461         dim = {width: 0};
462     
463     if (moz) {
464       this.mozTextStyle = this.buildStyle(style);
465       dim.width = this.mozMeasureText(text);
466     }
467     else {
468       var face = ctxt.getFaceFromStyle(style),
469           scale = (style.size / face.resolution) * (3/4);
470           
471       dim.width = this.getTextExtents(text, style).ha * scale * ctxt.scaling;
472     }
473     
474     return dim;
475   };
476 })();