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

TexText Bug: Some Chinese characters get hollowed #2146

Open
osMrPigHead opened this issue Jul 16, 2024 · 6 comments
Open

TexText Bug: Some Chinese characters get hollowed #2146

osMrPigHead opened this issue Jul 16, 2024 · 6 comments
Labels

Comments

@osMrPigHead
Copy link
Contributor

osMrPigHead commented Jul 16, 2024

English

Describe the bug

Some of the Chinese characters in TexText get hollowed, even when using SVGMoject with compiled SVG files (nothing wrong with SVG files). I thought it was a rendering bug of ManimGL.

Code:

from manimlib import *
 
 
class TestScene(Scene):
    def construct(self):
        self.add(TexText("TexText: 始终").to_edge(UP),
                 Text("Text: 始终").to_edge(DOWN))
        self.wait()

Wrong display or Error traceback:

Image_1721129240464

Additional context

Someone else (@起床王王王 in Manim Kindergarten's QQ group) found the same problem in Feb this year. He solved by changing typeface, but other characters can also get a display error in other typefaces on my computer. This seemed to have different effects on different computers. E.g. “边” and “代” got hollowed on @起床王王王's computer, but it worked well for me. And I got hollowed “始” and “终” in Song typeface, deformed “奇” in Kai typeface.

The picture shows @起床王王王's result.

86D97D238DC8AAF49E37F270A20FE243

中文

描述

TexText 中有些汉字显示为空心,哪怕把相应的 SVG 放到 SVGMobject 里去也是同样效果,但编译出的 SVG 没有任何异常,我觉得应该是 ManimGL 渲染的问题。

图片和源码见上文

附加信息

今年 2 月的时候 mk 群里面也有人(群昵称 @起床王王王)发现了同样问题,他当时是用更换字体的方式解决的,但我这里更换了字体就会有其他汉字显示异常。这个问题貌似在不同电脑上表现不同,在 @起床王王王 的电脑上“边”“代”有问题,但在我的电脑上显示正常,在我的电脑上用宋体会导致“始”“终”空心,用楷体会导致“奇”字形扭曲。

图(见上文)为 @起床王王王 的显示效果。

@osMrPigHead
Copy link
Contributor Author

English

ShowCreation can visualize how the character is "drawed" by SVG paths. It shows that “始” and “终” gets their filling not in the border of strokes, but in the gap between the strokes, such as the space in “口”, a component of “始”.

So I tried changing the order of different closed path in the SVG file (in the attribute d of the path element, between one "Z" and another is a closed path). The process of ShowCreation sure changed, however as long as I don't delete the inner or outer frame of “口”, I still get the wrong result.

TestScene.mp4

中文

ShowCreation 展现了SVG中汉字的“绘制”过程,可以发现,在我电脑上只有边框而没有填充的汉字(“始”“终”)在 ShowCreation 的创建过程中被错误地填充了像“口”这样的笔画之间的区域。

所以我调换了一下 SVG 里的绘制顺序(path 元素 d 属性中两个“Z”之间的部分是一个闭合路径),看能不能借此改变 ManimGL 对汉字的渲染,改了之后 ShowCreation 的绘制顺序确实是变了,但只要“始”里面那个“口”还是完整的,渲染就不正常,去掉“口”的外边框或内边框才能让“始”的其他部分渲染正常。

视频见上文

@osMrPigHead osMrPigHead changed the title TexText Bug: Some Chinese characters get hollowed TexText Bug: Some Chinese characters get hollowed Jul 17, 2024
@osMrPigHead
Copy link
Contributor Author

osMrPigHead commented Jul 17, 2024

English

Seems that the problem is caused by SVG fill-rule. ManimGL takes the nonzero fill-rule (see Understanding the SVG fill-rule Property). However, when both counterclockwise and clockwise curves exist in one SVG graph, ManimGL doesn't fill the area inside a clockwise curve even if the area isn't surrounded by a counterclockwise curve. And some characters got clockwise outer edges from dvisvgm, therefore they can't be filled.

Here's an example:

<!--a.svg-->
<?xml version='1.0' encoding='UTF-8'?>
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='16pt' height='16pt' viewBox='-4 -4 12 12'>
<defs>
<path id='test' d='M 0 0 L 0 7 L 7 7 L 7 0 L 0 0 Z M 1 1 L 6 1 L 6 6 L 1 6 L 1 1 Z M 2 2 L 5 2 L 5 5 L 2 5 L 2 2 Z M 3 3 L 3 4 L 4 4 L 4 3 L 3 3 Z'/>
</defs>
<g>
<use x='0' y='0' xlink:href='#test'/>
</g>
</svg>
# scene.py
from manimlib import *


class TestScene(Scene):
    def construct(self):
        self.play(ShowCreation(SVGMobject("a.svg", color="white"), run_time=10))

I created 4 squares in a.svg. The directions of them are (from the largest to the smallest) counterclockwise, clockwise, clockwise, counterclockwise.

This is what a.svg displays in Chrome:
image
But in ManimGL, it's filled incorrectly:
image
Notice that the background color in Chrome is white and that in ManimGL is black.

P.S. Interestingly, ShowCreation gave different drawing sequence to different directions in the SVG file. After I swapped the directions of the 2 smallest rectangles, ShowCreation replaced from-smallest-to-largest sequence with from-largest-to-smallest sequence.

中文

这个问题应该是 SVG 的 fill-rule 导致的,ManimGL 用的是 nonzero(参考 Understanding the SVG fill-rule Property)。但只要顺时针线和逆时针线存在于同一张图中,ManimGL 就不会填充处在顺时针线内的区域(哪怕这个区域的外面没有逆时针线),我这里没有填充的字外边缘都是顺时针方向,就出现了空心的问题。

例:

<!--a.svg-->
<?xml version='1.0' encoding='UTF-8'?>
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='16pt' height='16pt' viewBox='-4 -4 12 12'>
<defs>
<path id='test' d='M 0 0 L 0 7 L 7 7 L 7 0 L 0 0 Z M 1 1 L 6 1 L 6 6 L 1 6 L 1 1 Z M 2 2 L 5 2 L 5 5 L 2 5 L 2 2 Z M 3 3 L 3 4 L 4 4 L 4 3 L 3 3 Z'/>
</defs>
<g>
<use x='0' y='0' xlink:href='#test'/>
</g>
</svg>
# scene.py
from manimlib import *


class TestScene(Scene):
    def construct(self):
        self.play(ShowCreation(SVGMobject("a.svg", color="white"), run_time=10))

a.svg 里面有四个嵌套的绕行方向不同的正方形,从外到内的绕行方向分别是逆、顺、顺、逆时针。

Chrome 中显示效果是这样的:
image
ManimGL 中内部的两个正方形没被填充:
image
两张图片颜色是相反的,Chrome 中是白色背景黑色前景,ManimGL 中是黑色背景白色前景

注:只改变绕行方向也会影响 ShowCreation 展示的绘制顺序,比如我这里 a.svg 的绘制顺序是从外到内,但如果把绕行方向改成逆、顺、逆、顺,绘制顺序就会变成从内到外。

@3b1b
Copy link
Owner

3b1b commented Oct 11, 2024

Thanks for the detailed explanation and example. This will be tricky to fix fully on the rendering side without compromising some other features, but I can look into it. In the meantime, one potential strategy would be to edit how SVGMobject reads in SVGs such that it re-orients internal curves whenever there's this discrepancy.

3b1b added a commit that referenced this issue Oct 23, 2024
@3b1b 3b1b mentioned this issue Oct 23, 2024
@3b1b
Copy link
Owner

3b1b commented Oct 23, 2024

I think the most recent commit should address this. At the very least, the example you gave including the holes within holes renders properly now. Let me know if the Chinese text you mentioned renders better.

It's not perfect, I could find edge cases where mixed winding signs and partial opacity behave strangely together, but hopefully, that's a pathological enough case that it won't be as painful a bug.

@germanzhu
Copy link

Switching to the latest version has improved the overall situation significantly compared to before, but it is still not perfect. The specifics are as follows:
Testing was conducted using three Simplified Chinese fonts: Microsoft YaHei, Source Han Sans, and LXGW WenKai.
Testing was conducted on commonly used Chinese characters, and it was found that all three fonts have certain characters that cannot be displayed accurately.
Microsoft YaHei: 粗,当,衫,显
Source Han Sans: 蹲,给,论,诱,帐,紫
LXGW WenKai:侧,厕,测,答,钉,动,费,公,谷,浩,浑,简,洁,禁,坑,礼,虑,骡,票,洽,签,潜,衫,抬,踢,修,沿,言,冶,浴,钥,治,武

The test code is as follows:

from manimlib import *
 
 
class TestScene(Scene):
    def construct(self):
        preamble = r"""
        \usepackage{xeCJK}
        \renewcommand{\songti}{\CJKfontspec{LXGW WenKai Mono}}
        \renewcommand{\heiti}{\CJKfontspec{Microsoft YaHei}}
        \newcommand{\shs}{\CJKfontspec{Source Han Sans SC}}
        """
        ## 微软雅黑:  粗,当,衫,显
        ## 霞鹜文楷:  侧,厕,测,答,钉,动,费,公,谷,浩,浑,简,洁,禁,坑,礼,虑,骡,票,洽,签,潜,衫,抬,踢,修,沿,言,冶,浴,钥,治,武
        ## 思源黑体:  蹲,给,论,诱,帐,紫

        txt = r"""
        Microsoft YaHei: \heiti 粗当衫显

        LXGW WenKai Mono: \songti 侧厕测答钉动费公谷浩浑简洁禁坑礼虑骡票洽签潜衫抬踢修沿言冶浴钥治武

        Source Han Sans SC: \shs 蹲给论诱帐紫
        """
        self.add(TexText(txt, template="ctex", additional_preamble=preamble, font_size=36).to_edge(UP))
        self.wait()
TestScene.mp4

@3b1b
Copy link
Owner

3b1b commented Oct 24, 2024

Ah, I see. It seems like this negative winding number situation now interferes poorly with the border width. If you call text.set_fill(border_width=0), you'll notice the issue goes away. I'll think of a better way to have this addressed by default.

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

3 participants