background

In the project, there was a requirement for making electronic certificates, which needed to generate different pictures of certificates according to the certificate template and the text edited by users. Therefore, HTML2Canvas was used to convert DOM elements into canvas and regenerate them into pictures.

The problem

In the process of CONVERTING HTML to Canvas, if there is a quotation mark before the wrapping line of the text element (there is a problem with the quotation mark if all other symbols are fine), the corresponding text of the generated image will be wrapped and overlapped with the character at the beginning of the next line, as shown below

To solve the process

1. First try changing the CSS property values, including white-space, word-break, etc

2,GoogleFind a similar problem, someone through changing html2Canvas source codeSUPPORT_RANGE_BOUNDSA value offalseTo solve the problem. The test won’t work

3. But thenSUPPORT_RANGE_BOUNDSFound near the related code, there is a suspicious sentenceoffset += text.lengthDoubt the wholeparseTextBoundsMethod may be the culpritSo we went through it and printed it outtextList:It is found that common characters are separated separately, common symbols (such as the last exclamation mark) are merged with the previous character, but quotation marks specifically combine the latter character

4, so the direction to solve the problem is how to eliminate the quotation mark and the next character of the connection, in the code abovetextListSee the way to getletterRenderingThe judge:Try to changeletterRenderingThe value of the willparent.style.letterSpacing ! = = 0Make it congruent and print it latertextListNotice that symbols are separated separately, and the resulting image is finally free of quote wrapping and character overlap:

5. As you can see from the above code, letterRendering is defined by the parent element’s letter-spacing property. The CSS property of the paragraph element in my code does not have a letter-spacing value. The spacing is set to 0.1px without changing the html2Canvas source code

Analysis of the

Why is it that when quotation marks are attached to the next character in a textList, styles overlap?

1, the first idea is to knowtextListAfter the generation of use, from the following code can be seentextListMainly to servetextBoundsThe generation of:Print out what the method returned at the endtextBoundsContent:It can be seen that the function of this whole method is to calculate the position, width and height data of each group of characters divided, and the preliminary guess is used for drawing and generation of canvas

The printed data, however, is a bit strange:

These four data form the coordinate + width and height of the point in the upper left corner, which is the area occupied by the element, so it can be concluded that other groups are small areas occupied by a single word, while“RONIt occupies an area that spans two lines:Well, logically, iftextListThe data is really used to draw text, so draw out“RONShouldn’t it occupy the “get” position in the top left corner of the border? Why is it in the position of the word “rong”?

2. Then you need to be suretextListThe real use of

To find theparseTextBoundsMethod is used in the discovery of a newTextContainerObjectboundsAttribute values passed in:Then find theboundsProperty found that it is used in many places, observe the function name is mostly render XXX element, so here directly to hear the name of the most likelyrenderTextNodeMethod, and find in itboundsAs actx.fillTextParameter using:thenboundsThe idea that data is used to draw on a canvas proves it

3. Why is the text drawn in the lower left corner instead of the upper left corner? This is related to the fllText method of canvas, because the x and y coordinate points passed into fillText determine the textBaseline to draw the text, and the text naturally needs to be drawn above the baseline, so intuitively, this point determines the position of the lower left corner of the text. Therefore, You can see that HTML2Canvas uses top+height instead of just top when passing the y of the point

4. To sum up, “rong” will be drawn to the lower left corner of the area, which is the original position of the rong word, so in the case of other characters are accurately calculated and drawn according to the original position, it is natural that there will be character overlap

Why do quotation marks in particular enclose the preceding and following characters together?

You can seeletterRenderingWhen false, the text will be_Unicode.breakWordsMethod that would otherwise be used_Unicode.toCodePointsSplit and iterate through the calls one by one_Unicode.fromCodePointmethodsFurther found_Unicode.toCodePointsThis is done by breaking characters into Unicode encoding one by one:In the after_Unicode.fromCodePointThe code is then converted into a list of characters, so that the characters separated are completely independent

_Unicode.breakWordsMore complex, after a period of time after access to its callcss-line-breakThe character division method of the module, in which various symbols are treated specially to keep them connected to the previous character:Only the double quotation marks are not broken, even after the character:

🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻 🤷 🏻

👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎 👎

Related to the source code

parseTextBounds
var parseTextBounds = exports.parseTextBounds = function parseTextBounds(value, parent, node) {
  varletterRendering = parent.style.letterSpacing ! = =0;
  var textList = letterRendering ? (0, _Unicode.toCodePoints)(value).map(function (i) {
    return (0, _Unicode.fromCodePoint)(i);
  }) : (0, _Unicode.breakWords)(value, parent);
  var length = textList.length;
  var defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null;
  var scrollX = defaultView ? defaultView.pageXOffset : 0;
  var scrollY = defaultView ? defaultView.pageYOffset : 0;
  var textBounds = [];
  var offset = 0;
  for (var i = 0; i < length; i++) {
    var text = textList[i];
    if(parent.style.textDecoration ! == _textDecoration.TEXT_DECORATION.NONE || text.trim().length >0) {
      if (_Feature2.default.SUPPORT_RANGE_BOUNDS) {
        textBounds.push(new TextBounds(text, getRangeBounds(node, offset, text.length, scrollX, scrollY)));
      } else {
        var replacementNode = node.splitText(text.length);
        textBounds.push(newTextBounds(text, getWrapperBounds(node, scrollX, scrollY))); node = replacementNode; }}else if(! _Feature2.default.SUPPORT_RANGE_BOUNDS) { node = node.splitText(text.length); } offset += text.length; }return textBounds;
};
Copy the code
TextContainer
var TextContainer = function () {
  function TextContainer(text, parent, bounds) {
    _classCallCheck(this, TextContainer);

    this.text = text;
    this.parent = parent;
    this.bounds = bounds;
  }

  _createClass(TextContainer, null[{key: 'fromTextNode'.value: function fromTextNode(node, parent) {
      var text = transform(node.data, parent.style.textTransform);
      return new TextContainer(text, parent, (0, _TextBounds.parseTextBounds)(text, parent, node)); }}]);returnTextContainer; } ();Copy the code
renderTextNode
function renderTextNode(textBounds, color, font, textDecoration, textShadows) {
  var _this4 = this;

  this.ctx.font = [font.fontStyle, font.fontVariant, font.fontWeight, font.fontSize, font.fontFamily].join(' ');

  textBounds.forEach(function (text) {
    _this4.ctx.fillStyle = color.toString();
    if (textShadows && text.text.trim().length) {
      textShadows.slice(0).reverse().forEach(function (textShadow) {
        _this4.ctx.shadowColor = textShadow.color.toString();
        _this4.ctx.shadowOffsetX = textShadow.offsetX * _this4.options.scale;
        _this4.ctx.shadowOffsetY = textShadow.offsetY * _this4.options.scale;
        _this4.ctx.shadowBlur = textShadow.blur;

        _this4.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
      });
    } else {
      _this4.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
    }
  // ...
Copy the code