Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Font .width & .height not accurate #1108

Open
PvanHengel opened this issue Jan 19, 2014 · 18 comments
Open

Custom Font .width & .height not accurate #1108

PvanHengel opened this issue Jan 19, 2014 · 18 comments
Labels

Comments

@PvanHengel
Copy link

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
Copy link
Member

asturur commented Nov 24, 2014

@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
Copy link
Member

asturur commented Nov 28, 2014

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

@asturur
Copy link
Member

asturur commented Dec 5, 2014

add issue #1878

@lukejagodzinski
Copy link

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
Copy link

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

Thanks

@terrancesnyder
Copy link

+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
Copy link

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
Copy link

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
Copy link

mukeshcp commented Aug 4, 2016

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 :

  • Text Object exact height & width after rotation
  • top value of toppest,leftest,bottemest charater of text in canvas

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
Copy link

terrancesnyder commented Aug 7, 2016

@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
Copy link

@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
Copy link

@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
Copy link

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

@fahadnabbasi
Copy link

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

@Arx9
Copy link

Arx9 commented Jun 7, 2018

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

@mh5
Copy link

mh5 commented May 26, 2020

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
Copy link
Member

asturur commented May 26, 2020

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
Copy link

lk77 commented Oct 26, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests