fabricjs / fabric.js

Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
http://fabricjs.com
Other
28.15k stars 3.45k forks source link

Custom Font .width & .height not accurate #1108

Open PvanHengel opened 10 years ago

PvanHengel commented 10 years ago

Hi, we are using custom fonts on node.js, it seems that when using custom fonts the width returned by textItem.width and textItem.height do not match what is actually rendered. It works fine with the default font.

Here is some sample code bits:

var font1 = new canvas.Font('Symbola605', '/fonts/truetype/ttf-ancient-scripts/Symbola605.ttf');

canvas.contextContainer.addFont(font1);

var text = new fabric.Text("testing 123" , { fontSize: 45 ,textAlign: 'center' ,fontFamily: 'Symbola605' ,backgroundColor:'silver' });

console.log(">>>>>>>>> " + text.width + " X " + text.height + " <<<<<<<<<");

The log does not match the actual width of the text when measured after being rendered in the png.

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

asturur commented 9 years ago

@kirschkern , i report here your issue from #1844

The text element can be larger than its actual width property so the text has some overflow. See it here => http://jsfiddle.net/tQCTd/5/ on the "r" character. The selection border, which visualizes the elements width, "cuts" off the "r" at the right side. How can I get the real width for the text element?

I need this as I use the text element only as a wrapper to draw on a hidden canvas, get the image data from it and do some image manipulation on it. (Mainly removing anti-aliasing and adding a border.) However, when using the width of the text element to size my hidden canvas, some characters get cut off.

asturur commented 9 years ago

add issue #664 and close it http://jsfiddle.net/3BAkN/21/

asturur commented 9 years ago

add issue #1878

lukejagodzinski commented 9 years ago

Some fonts like for example Pacifico have kerning set no to the letter width but a little bit smaller so that letters can connect with each other. In browsers measureText is calculating text width including kerning so it's wrong value. If you load this font to OpenType.js it shows exactly what I mean. Actually we could use OpenType.js to render fonts. It works also in Node.js. It returns Path for given text and you can just render it. You have many options of calculating text width/height there. However it's not working in old versions of IE (don't know which one).

If not using OpenType.js there is also problem with calculating text height. Right now you make assumption that text height is equal to font size but it's not true in all cases. In new specification of HTML there is extended measureText function. With this function you can calculate real value of font height but it's not supported by any browser. There are polyfills for that. The one I'm right now creating https://github.com/jagi/measure-text-polyfill but it's not yet ready. There is nice polyfill https://github.com/motiz88/canvas-text-metrics-polyfill but it's not compatible with standard and there will be some problems in Node.js. NodeCanvas has extended measureText function but it's not returning correct values.

Concluding, in my opinion the best option would be using OpenType.js.

gabrielstuff commented 9 years ago

I confirm that this issue is still blocking. Anyone working on it ? What do you think about the solution using opentype.js ?

Thanks

terrancesnyder commented 8 years ago

+1 for opentype it works quite well for font rendering especially nodejs + windows to avoid having to include pango/freetype/etc and renders pixel perfect as you could expect.

right now I'm doing the same sort of polyfill for canvas.measureText.

See here as well for other like minded people https://developer.tizen.org/community/tip-tech/working-fonts-using-opentype.js?langswitch=en

kirschkern commented 8 years ago

Using custom fonts, special characters like the german umlaute might have the wrong size values. See http://jsfiddle.net/tQCTd/9/

The top border is right in the middle of the dots on top of the characters. This worked in Version 1.4.13.

terrancesnyder commented 8 years ago

I've gotten a general shim to work with openType.js and fabric (both nodeJS and browser).

This relies on having a "Font.js" file that is able to resolve a openType font definition given the family name of the font. This allows full rendering (however, edit mode may be crippled). This gives pixel perfect rendering of fonts of any type (japanese, kerned, etc).

    // override filltext so we get best rendering of fonts via opentype
    $scope.canvas.fillText = $scope.canvas.contextContainer.fillText = function(text, top, left) {
      var font = Font.resolve($scope.canvas.contextContainer.font);
      var path = font.getPath(text, top, left, font.size);
      path.fill = $scope.canvas.contextContainer.fillStyle;
      path.draw($scope.canvas.contextContainer);
    };

    $scope.canvas.measureText = $scope.canvas.contextContainer.measureText = function(text, size) {
      var font = Font.resolve($scope.canvas.contextContainer.font);
      return font.measureText(text, size);
    };

The font.js is a relatively simple class - simply using OpenType to resolve the TTF/OTF and giving it a hash lookup. From node we simply walk a directory and preload all the fonts into a hash. For browser we resolve the first time and then cache in one area. In others we use google font loader.

var Font = {
   resolve: function(name) {
       // .. find the font using your own repo/lookup
      //  opentype.load('fonts/' + name + '.ttf', function(err, font) {
      //    if (err) throw err;
      //    cache[name] = font;
      // });
      return cache[name];
   }
}
mukeshcp commented 7 years ago

Hi, I rendered text with openty.js in Fabric js. But its working very slow. Also I calculated height & width By using drawMetrics function.its works. But very slow.

I need below value after rotation :

can anybody help to me to get all values by small code or any function which work smoothly ?

Please find attachment what exactly i need.

textissue

Thanks, Mukesh

terrancesnyder commented 7 years ago

@mukeshcp i'll be taking a look at this over the next month. But after exploring the canvas code for line breaks another issue is in the words as well.

https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages

Specifically the _wrapLine(...) call inside iText assumes that the words being wrapped are english written system so that if I typed in japanese it would not take into account word wrapping rules here. For example:

おひおよございます

There are no white-space characters for this.

http://www.localizingjapan.com/blog/2012/01/20/regular-expressions-for-japanese-text/

capture

Verification that custom fonts in the browser render their line breaks and font kerning exactly as they do in node (using opentype shim). Its a bit ugly on the node side as I can't seem to override ctx.measureText(...) to create polyfill - seems like it just wont attach. Instead i've got a shim that checks for the extance of openTypeMeasureText(...) ... sadly in the global scope. But it works, and that's all I care about right now.

The main change to fabric is below - note in the _wrapLine(..) function replacement we dont do it by character (char[0].width + char[1].width + char[2].width) as that destroys kerning information http://type.method.ac/. Specifically "AVAVAVAVAV" becomes invalid as measuring "A" by itself and "V" by itself doesnt take into account kerning... So we need to measure as we build a buffer.

The bad in this is that the assumption on when we effectively can take a break, the marker is currently based on a very very basic english written system (and was before), checking for spaces to determine "hey maybe we can break here" and then proceeds to measure the next collection of characters, if we overflow the box, we break back to the last candidate line breakpoint.

This needs to be extended with other language break options (probably allowing custom injection of break 'rules') by an external party? Such as new fabric.Textbox({ ...., line_break_rules: function(text) { });?

    /**
     * Wraps a line of text using the width of the Textbox and a context.
     * @param {CanvasRenderingContext2D} ctx Context to use for measurements
     * @param {String} text The string of text to split into lines
     * @param {Number} lineIndex
     * @returns {Array} Array of line(s) into which the given text is wrapped
     * to.
     */
    _wrapLine: function(ctx, text, lineIndex) {
      var wrapped_lines = [];
      var line_buffer = '';
      var candidate_line = '';

      var max_line = { width: 0.00, text: '' };

      // http://www.rikai.com/library/kanjitables/kanji_codes.unicode.shtml
      // http://www.tamasoft.co.jp/en/general-info/unicode.html
      // https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages
      var lang_jpn = /[\u3000-\u303F]|[\u3040-\u309F]|[\u30A0-\u30FF]|[\uFF00-\uFFEF]|[\u4E00-\u9FAF]|[\u2605-\u2606]|[\u2190-\u2195]|\u203B/g.test(text);
      var jpn_punctuation_or_special = /[\u3000-\u303F]|[\uFF5B-\uFF65]|[\uFFBF-\uFFEF]|[$&+,:;=?@#|'<>.^*()%!-]|[\s]/g;

      var text_length = text.split('').length;

      for (var i=0; i < text_length; i++) {
        line_buffer += text[i];

        // TODO... better handling here of international text languages        
        if (lang_jpn) {
          candidate_line = line_buffer.trim();
        } else {
          if (text[i] == ' ') {
            candidate_line = line_buffer.trim();
          }
        }

        var m = openTypeMeasureText ? openTypeMeasureText(line_buffer, null, ctx) : ctx.measureText(line_buffer);

        if (m.width >= this.width && candidate_line.trim().length > 0) {

          // if we run japanese than we need to check if the last character overlaps, if so
          // we need to move to the next line, but only if the last character is not a japanese
          // punctuation
          var last_char = candidate_line.trim().substring(candidate_line.trim().length-1);
          if (lang_jpn && false == jpn_punctuation_or_special.test(last_char)) {
            var shifted_line = candidate_line.trim().substring(0, candidate_line.trim().length-1);
            wrapped_lines.push(shifted_line);
            line_buffer = (last_char + line_buffer.substring(candidate_line.trim().length+1).trim())+'';
          } else {
            wrapped_lines.push(candidate_line.trim());
            line_buffer = (line_buffer.substring(candidate_line.trim().length).trim())+'';
          }

          candidate_line = '';
          if (max_line.width < m.width) {
            max_line.width = m.width;
            max_line.text = candidate_line;
          }
        }
      }
      if (line_buffer.trim().length > 0) {
        wrapped_lines.push(line_buffer);
      }

      if (max_line.text.length > this.dynamicMinWidth) {
        this.dynamicMinWidth = max_line.text.length;
      }

      return wrapped_lines;
    },

update

Japanese text word wrapping... I think it's best to allow an external party to inject their word wrapping rules and detection as this can get nasty...

jpn

Note in the below we avoid wrapping on the punctuation as that is not allowed in japanese...

jpn_punc

fahadnabbasi commented 7 years ago

@terrancesnyder I tried your method and it works fine but the problem is how can I define styles like bold and italic? I really need some guidelines here so any help would be appreciated

fahadnabbasi commented 7 years ago

@terrancesnyder another issue, I have

var fabric = require('fabric'); snapshotCanvas = fabric.createCanvasForNode(10,10); so now with it, how can I override

I tried this but not calling my own function

var orig_drawImage = snapshotCanvas.contextContainer.fillText;

console.log("``````````````````````````````````````````````````````")
snapshotCanvas.fillText=snapshotCanvas.contextContainer.fillText = function(text, left, top,  ctx, snapshotCanvasObj, style)
{
terrancesnyder commented 7 years ago

@fahadnabbasi bold and italic are usually the names of the fonts themselves - you might have two different TTFs

fahadnabbasi commented 7 years ago

@terrancesnyder thanks now I understand the whole concept and implemented it without any problem :)

Arx9 commented 6 years ago

it's possible iText can rendering text use writing-mode: tb-rl css style?

mh5 commented 4 years ago

The issue is resolved for me using a shim that overrides the canvas functions measureText(), fillText(), and strokeText() with implementations that match those of opentype.js. I'm not sure about the performance as I render just a couple of words or a sentence rather than paragraphs. Maybe we can integrate such implementations and have a setting to invoke them instead of the native canvas functions?

I currently got this working by loading the shim

<script src="canvas-text-opentypejs-shim.js"></script>
<!-- can be obtained from https://github.com/shyiko/canvas-text-opentypejs-shim/blob/master/canvas-text-opentypejs-shim.js -->

... and then applying it to the native canvas

  var use_opentype_for_canvas = window['canvas-text-opentypejs-shim'];
  var resolveFont = function(o) {
    return opentypeFontInstance;
  };
  use_opentype_for_canvas(canvas.getContext('2d'), resolveFont);

And you can then you can wrap that canvas object in a fabric.Canvas and use it as usual.

asturur commented 4 years ago

even if speed wouldn't be great, would be an option for many cases. Also fabric generally measure the font once, and maybe there is the reason of some bugs too.

lk77 commented 3 years ago

Hello,

i'm doing a toDataURL to generate png from text, and text was cropped, i didn't find a real solution, what i've done is a set({height: o.height*1.1}) to increase the height of the element, it does not change the position of the text, and it gives enough margin to not be cropped.