mozman / ezdxf

Python interface to DXF
https://ezdxf.mozman.at
MIT License
926 stars 190 forks source link

Proportional font rendering issues #333

Closed mozman closed 3 years ago

mozman commented 3 years ago

The magenta text frames are calculated by ezdxf with matplotlib support.

Arial Regular BricsCAD vs cad_viewer.py - this is very good: image

Arial Narrow - cad_viewe.py stretches the text too wide: image

Arial Black perfect: image

The example above shows also a problem of BOTTOM text alignment. BricsCAD (same in TrueView) renders the text with more space between baseline and descender:

Arial Regular: image

Arial Black: image

The font metrics do not show a difference which could explain this behavior. I guess this is not solvable with the tools we have.

Font measurements for Arial Regular:

Style name: ARIAL
Font name: arial.ttf
baseline: 0.0
cap_height: 1.0
x_height: 0.7245142981881685
descender_height: 0.2776686313032089

Font measurements for Arial Black:

Style name: Arial Black
Font name: ariblk.ttf
baseline: 0.0
cap_height: 1.0
x_height: 0.7245142981881685
descender_height: 0.27570399476096924

And now really strange things

Wrong TOP text alignment for font "Baby Kruffy": image

Font measurements:

Style name: Baby Kruffy
Font name: babyk___.ttf
baseline: 0.0
cap_height: 1.0
x_height: 0.9568982105060613
descender_height: 0.2832403309601693

Letters: "Xxp" :-) image

Font Bouton, where the current font measurement can not succeed: image

Letters: "Xxp" :-) image

Style name: BOUTON
Font name: BOUTON_International_symbols.ttf
baseline: 0.0
cap_height: 1.0
x_height: 1.0
descender_height: 0.008861940298507462

Something positive at the end - regular fonts work very well:

image

image

Without the text frames it is hard to notice the difference to CAD applications: image

mozman commented 3 years ago

And cad_viewer.py renders SHX fonts nicer than BricsCAD:

image

The marked text for the font TXT.SHX looks good here (see #332), but the magic is that this is not the TXT.SHX font, it is the ROMANS.SHX font which maps to "romans__.ttf"

mozman commented 3 years ago

I recognized yet that we have the possibility to vectorize text: image

The red polylines shows the TextPath.vertices, the white polylines are the polylines created by TextPath.to_polygons(). As you see the (0, 0) is an artifact and not a real boundary point of the char.

The Arial font used here uses only quadratic bezier curves, which I haven't implemented in my Path class, but this would be easy to add, because quadratic bezier curves are much simpler and faster than cubic bezier curves. This would add text vectorization support with the help of matplotlib.

mozman commented 3 years ago

First string to ezdxf Path conversion using matplotlib, exported as LWPOLYLINE entities:

image

Example: https://github.com/mozman/ezdxf/blob/master/examples/text_string_to_path.py

mozman commented 3 years ago

... and as HATCH entities: image

This is finally a function that I can use in my job :-). It is not possible in Allplan to create transparent text for watermarks, and even exploding text in BricsCAD creates only outlines (LWPOLYLINES), which have to be filled afterwards.

mozman commented 3 years ago

There seems to be an text rendering issue in the drawing add-on.

The red text is rendered by BricsCAD, the yellow outline is created by the text2path add-on using the matplotlib TextPath() rendering and the result is very close:

image

The same text rendered by cad_viewer.py: image

example script: https://github.com/mozman/ezdxf/blob/master/examples/addons/text_entity_to_path.py

DXF file: entity2path.zip

mozman commented 3 years ago

This example shows the limits of my current approach for ALIGNED and FIT alignments:

image

The text created by text2path (yellow) is exact 12 drawing units wide as defined by insert=(0, 0) and align_point=(12, 0), but TrueView and BricsCAD render the text (red) a little bit narrower and with an offset to (0, 0).

The text2path add-on scales the paths by their bounding box extents. I have no idea if it's possible to render a text to an exact length by the matplotlib TextPath() render tool, it only supports a size parameter, but no width factor or anything similar to scale the length of the text.

This is an example where ezdxf is correct, but does not match the result of CAD applications.

PS: cad_viewer.py: FIT is perfect and ALIGNED has an incorrect text height. Which means the solution for the stretching problem is already solved in the drawing add-on! image

mozman commented 3 years ago

ALIGNED is solved, was my fault by introducing an arbitrary scaling factor, but this factor was necessary for at least one example: image

mozman commented 3 years ago

draw_cad.py is OK (perfect!) - the error has to be in the PyQt5 backend used by cad_viewer.py: image

image

PS: Results in cad_viewer.py depend on the font: image

mozman commented 3 years ago

Stretch factor of QFont was not set in the pyqt backend, but doing so makes the result worse :-(

image

mbway commented 3 years ago

if you want to go down this route, in my own code I'm using matplotlib to render the text paths and using Qt to display them. This way I can achieve the same results when rendering to images or displaying, but the matplotlib text rendering is unfortunately slower.

from PyQt5 import QtGui as qg
from matplotlib.path import Path as MatplotlibPath

def matplotlib_path_to_qt_path(path: MatplotlibPath) -> qg.QPainterPath:
    out = qg.QPainterPath()
    assert len(path.vertices) == len(path.codes)
    points = list(reversed(path.vertices.tolist()))
    codes = list(reversed(path.codes))
    while codes:
        code = codes.pop()
        if code == MatplotlibPath.STOP:
            points.pop()
        elif code == MatplotlibPath.MOVETO:
            out.moveTo(*points.pop())
        elif code == MatplotlibPath.LINETO:
            out.lineTo(*points.pop())
        elif code == MatplotlibPath.CURVE3:
            (x1, y1), (x2, y2) = points.pop(), points.pop()
            assert codes.pop() == MatplotlibPath.CURVE3
            out.quadTo(x1, y1, x2, y2)
        elif code == MatplotlibPath.CURVE4:
            (x1, y1), (x2, y2), (x3, y3) = points.pop(), points.pop(), points.pop()
            assert codes.pop() == MatplotlibPath.CURVE4
            assert codes.pop() == MatplotlibPath.CURVE4
            out.cubicTo(x1, y1, x2, y2, x3, y3)
        elif code == MatplotlibPath.CLOSEPOLY:
            points.pop()  # will be (0, 0)
            out.closeSubpath()
        else:
            raise ValueError(code)
    return out
mozman commented 3 years ago

The drawing add-on is more your baby :-).

For me the current status is good enough as this is only a problem for certain fonts, hopefully a minority of the fonts used in real drawings.

Luckily the default fonts which I have chosen, do not look that bad. I think we are as close to the (AutoCAD) standard as we can with reasonable effort (yellow outlines by matplotlib):

image