fabricjs / fabric.js

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

[Bug]: TextBox multi-line text textAlign: justify does not take effect #9132

Open trouvaille2023 opened 1 year ago

trouvaille2023 commented 1 year ago

CheckList

Version

6.0.0-beta10

In What environments are you experiencing the problem?

Chrome

Node Version (if applicable)

20.0.0

Link To Reproduction

See steps

Steps To Reproduce

  1. In the TextBox component, after opening multi-line text, justify both ends (textAlign: justify) is not the expected result.
  2. Text alignment. Possible values: “left”, “center”, “right”, “justify”, “justify-left”, “justify-center” or “justify-right”.

node version: 16.15.0 , 20.5.0 browser: Google Chrome 115 platform: MacOS 13. Windows 10 fabricjs version : 5.3.0, 6.0.0-beta10 and all versions

fabricjs:

image

Microsoft Word and css style:

image

My code:


function App() {
    let txt = `白鹤滩-浙江±800千伏特高压直流输电工程(下称白浙线)是全球首个混合级联特高压直流工程,在世界上首次研发“常规直流+柔性直流”的混合级联特高压直流输电技术,集成特高压直流输电大容量、远距离、低损耗,以及柔性直流输电控制灵活、系统支撑能力强的优势,示范引领意义重大。"`;
    txt = txt.repeat(5);
    const init = (selector: any) => {
        const dom = document.querySelector(selector) as HTMLDivElement;
        const ele = document.createElement('canvas') as HTMLCanvasElement;
        dom.replaceChildren(ele);

        const canvas = new Canvas(ele, {
            width: 700,
            height: 800,
        });
        const textbox = new Textbox(txt, {
            fontSize: 24,
            width: 700,
            splitByGrapheme: true,
            textAlign: 'justify',
        });
        canvas.add(textbox);
        canvas.renderAll();
    }
    useEffect(() => {
        init('#box')
    }, [])

    return (
        <div className="App" id='box'>
        </div>
    );
}
export default App;

Expected Behavior

It can be aligned on both ends of the rest of the lines except the last line like Microsoft Word or css style

image

Actual Behavior

When I set textAlign to justify, the right side is not aligned

image

Error Message & Stack Trace

None
asturur commented 1 year ago

This happens because justify is based on spaces. I suppose the language you are using (japanese?) is not using any space.

Are you using splitByGrapheme to wrap the text? or manually hitting enter?

trouvaille2023 commented 10 months ago

This happens because justify is based on spaces. I suppose the language you are using (japanese?) is not using any space.

Are you using splitByGrapheme to wrap the text? or manually hitting enter?

  1. The language is Chinese! Chinese! Chinese!, not japanese.
  2. I can't hitting enter manually.
  3. I used splitByGrapheme to wrap the text.
  4. I have provided the code case, please review it carefully.
function App() {
    let txt = '白鹤滩-浙江±800千伏特高压直流输电工程(下称白浙线)是全球首个混合级联特高压直流工程,在世界上首次研发“常规直流+柔性直流”的混合级联特高压直流输电技术,集成特高压直流输电大容量、远距离、低损耗,以及柔性直流输电控制灵活、系统支撑能力强的优势,示范引领意义重大。"';
    txt = txt.repeat(5);
    const init = (selector: any) => {
        const dom = document.querySelector(selector) as HTMLDivElement;
        const ele = document.createElement('canvas') as HTMLCanvasElement;
        dom.replaceChildren(ele);

        const canvas = new Canvas(ele, {
            width: 700,
            height: 800,
        });
        const textbox = new Textbox(txt, {
            fontSize: 24,
            width: 700,
            splitByGrapheme: true,
            textAlign: 'justify',
        });
        canvas.add(textbox);
        canvas.renderAll();
    }
    useEffect(() => {
        init('#box')
    }, [])

    return (
        <div className="App" id='box'>
        </div>
    );
}
export default App;
asturur commented 10 months ago

This happens because justify is based on spaces. I suppose the language you are using (japanese?) is not using any space. Are you using splitByGrapheme to wrap the text? or manually hitting enter?

  1. The language is Chinese! Chinese! Chinese!, not japanese.
  2. I can't hitting enter manually.
  3. I used splitByGrapheme to wrap the text.
  4. I have provided the code case, please review it carefully.
function App() {
    let txt = '白鹤滩-浙江±800千伏特高压直流输电工程(下称白浙线)是全球首个混合级联特高压直流工程,在世界上首次研发“常规直流+柔性直流”的混合级联特高压直流输电技术,集成特高压直流输电大容量、远距离、低损耗,以及柔性直流输电控制灵活、系统支撑能力强的优势,示范引领意义重大。"';
    txt = txt.repeat(5);
    const init = (selector: any) => {
        const dom = document.querySelector(selector) as HTMLDivElement;
        const ele = document.createElement('canvas') as HTMLCanvasElement;
        dom.replaceChildren(ele);

        const canvas = new Canvas(ele, {
            width: 700,
            height: 800,
        });
        const textbox = new Textbox(txt, {
            fontSize: 24,
            width: 700,
            splitByGrapheme: true,
            textAlign: 'justify',
        });
        canvas.add(textbox);
        canvas.renderAll();
    }
    useEffect(() => {
        init('#box')
    }, [])

    return (
        <div className="App" id='box'>
        </div>
    );
}
export default App;

I m sorry for the confusion, is hard for me to recognize Chinese from Japanese unless i see that 2 or 3 glyphs i can recognize.

Said so, if there are no spaces, fabricJS does not know how to justify because the rules for rendering are based on english language.

I want to review this, but this is not something we can fix easily

trouvaille2023 commented 10 months ago

This happens because justify is based on spaces. I suppose the language you are using (japanese?) is not using any space. Are you using splitByGrapheme to wrap the text? or manually hitting enter?

  1. The language is Chinese! Chinese! Chinese!, not japanese.
  2. I can't hitting enter manually.
  3. I used splitByGrapheme to wrap the text.
  4. I have provided the code case, please review it carefully.
function App() {
    let txt = '白鹤滩-浙江±800千伏特高压直流输电工程(下称白浙线)是全球首个混合级联特高压直流工程,在世界上首次研发“常规直流+柔性直流”的混合级联特高压直流输电技术,集成特高压直流输电大容量、远距离、低损耗,以及柔性直流输电控制灵活、系统支撑能力强的优势,示范引领意义重大。"';
    txt = txt.repeat(5);
    const init = (selector: any) => {
        const dom = document.querySelector(selector) as HTMLDivElement;
        const ele = document.createElement('canvas') as HTMLCanvasElement;
        dom.replaceChildren(ele);

        const canvas = new Canvas(ele, {
            width: 700,
            height: 800,
        });
        const textbox = new Textbox(txt, {
            fontSize: 24,
            width: 700,
            splitByGrapheme: true,
            textAlign: 'justify',
        });
        canvas.add(textbox);
        canvas.renderAll();
    }
    useEffect(() => {
        init('#box')
    }, [])

    return (
        <div className="App" id='box'>
        </div>
    );
}
export default App;

I m sorry for the confusion, is hard for me to recognize Chinese from Japanese unless i see that 2 or 3 glyphs i can recognize.

Said so, if there are no spaces, fabricJS does not know how to justify because the rules for rendering are based on english language.

I want to review this, but this is not something we can fix easily

In word processing, Chinese takes a Chinese character as the word boundary, and a Chinese character is a Chinese character without the need to use spaces to divide word boundaries. For example, in Chinese, "你好" means two Chinese characters.

eg:

const str = '我是中国人'
const res = str.split('')
cosnole.log(res)

The result out:   ['我', '是', '中', '国', '人']
more-strive commented 5 months ago

I have rewritten the enlargeSpaces function in fabric.js here
demo: https://yft.design
image

import { Textbox as OriginTextbox, classRegistry } from "fabric"

export class Textbox extends OriginTextbox {

  enlargeSpaces() {
    let diffSpace,
      currentLineWidth,
      numberOfSpaces,
      accumulatedSpace,
      line,
      charBound,
      spaces;
    for (let i = 0, len = this._textLines.length; i < len; i++) {
      if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) {
        continue;
      }
      accumulatedSpace = 0;
      line = this._textLines[i];
      currentLineWidth = this.getLineWidth(i);
      if (currentLineWidth < this.width && (spaces = this.textLines[i].split(''))) {

        numberOfSpaces = spaces.length - 1;
        diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
        for (let j = 0; j <= line.length; j++) {
          charBound = this.__charBounds[i][j];
          charBound.width += diffSpace;
          charBound.kernedWidth += diffSpace;
          charBound.left += accumulatedSpace;
          accumulatedSpace += diffSpace;
        }
      }
    }
  }
}

classRegistry.setClass(Textbox, 'Textbox')