Implemented text on a path SVG element#22
Conversation
cduck
left a comment
There was a problem hiding this comment.
Thanks for contributing. The TextOnPath class works fine for a standalone implementation, but to integrate with the package, I would rather you add a path= argument to Text. Can you see if it's possible to make all the current text alignment options work on a path? (x and y could be an offset instead of an absolute position.) If this turns out to be too complex then the current solution is good.
|
Some progress path = draw.Path(stroke='lightblue', fill='white')
path.M(50, 100-50).C(100, 100-0, 200, 100-100, 250, 100-50)
d.append(path)
x = 150
y = 50
text = draw.Text('Text on a path.', 24, x=x, y=y, path=path, center=False)path = draw.Path(stroke='lightblue', fill='white')
path.M(50, 100-50).C(100, 100-0, 200, 100-100, 250, 100-50)
d.append(path)
x = 150
y = 50
text = draw.Text('Text on a path.', 24, x=x, y=y, path=None, center=True)We should also consider adding But before that, first need to resolve an issue, I noticed that as long as we provide a manually defined path = draw.Circle(150, 50, 45, stroke='lightblue', fill='white')
d.append(path)
x = None
y = None
text = draw.Text('Text on a path.', 24, x=x, y=y, path=path, center=False) |
|
Good progress. What is the second output supposed to be? I would expect the text to follow the path but centered between the two ends. I don't think SVG supports text on a |
|
Well I think it should support it... |
|
That documentation says "a path referencing a circle". I assume this means a path element with two A commands (the A command draws an elliptical arc). Otherwise how do you control the start point? If you want, you could add an additional feature to here that (implicitly or explicitly) converts any supported shape into a path to make your example above work. |
|
@cduck I implemented attributes specific to import drawSvg as draw
len_x = 300
len_y = 300
d = draw.Drawing(len_x, len_y, origin='center', displayInline=False)
r = 50
cx = len_x/4
cy = len_y/4
ix = cx-r
iy = cy
fx = cx+r
fy = cy
d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
x = None
y = None
text = draw.Text('just some text', 10, x=x, y=y, path=path)
d.append(text)
cx = -len_x/4
cy = len_y/4
ix = cx-r
iy = cy
fx = cx+r
fy = cy
d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
text = draw.Text('10% offset', 10, x=x, y=y, path=path, startOffset='10%')
d.append(text)
cx = len_x/4
cy = -len_y/4
ix = cx-r
iy = cy
fx = cx+r
fy = cy
d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
text = draw.Text('inside text', 10, x=x, y=y, path=path, side='right', spacing='auto', method='stretch')
d.append(text)
cx = -len_x/4
cy = -len_y/4
ix = cx-r
iy = cy
fx = cx+r
fy = cy
d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
text = draw.Text('just some centered text', 10, x=x, y=y, path=path, text_anchor='middle', startOffset='50%')
d.append(text)
d.setRenderSize(400)
d.rasterize()
d.saveSvg('examples/example8.svg')
d.savePng('examples/example8.png')built-in what I see in Firefox if I open the output svg It seems like the Maybe we could find some hack to make it work regardles? Like document to do path in reverse for example. |
|
That makes sense. I think it would be possible to create a path with the reverse direction to simulate that but I suggest leaving that to a later PR. Try to keep this PR focused on adding the base function of text on a path. I suggest adding |
|
@cduck let me know what you think of this example, if it is not too long etc. I decided to stay with closed path examples as we keep the import drawSvg as draw
import math
len_x = 300
len_y = 300
d = draw.Drawing(len_x, len_y, origin='center')
# radii of the circles
R = 85
r = 65
# x, y are None when path argument provided for Text node
x = None
y = None
# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2
# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('text on a closed path with 10% offset', 10, x=x, y=y, path=path, startOffset='10%')
d.append(text)
# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some text on a closed path', 10, x=x, y=y, path=path)
d.append(text)
# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 50% offset', 10, x=x, y=y, path=path, text_anchor='middle', startOffset='50%')
d.append(text)
# text on an arbitrary path
ix = -len_x/3
iy = -len_y/4
path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/3, len_y, len_x/3, -len_y, len_x-len_x/6, len_y/2)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('it can be used for any path element, including paths that are not closed', 10, x=x, y=y, path=path)
d.append(text)
d.rasterize()I'll be happy to make further updates based on your feedback. |
|
Looking good. Did you find a way to adjust the vertical position of the text relative to the path (e.g. have the center of each character on the path)? Or is that not possible? I'll review the code when I have some time in the next few days. |
|
That was a good question. I found a way to achieve that, basically:
import drawSvg as draw
import math
len_x = 300
len_y = 300
d = draw.Drawing(len_x, len_y, origin='center')
# radii of the circles
R = 85
r = 65
# x, y are None when path argument provided for Text node
x = None
y = None
# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2
# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('text on a closed path with 10% offset and increased letter spacing', 10, x=x, y=y, path=path, startOffset='10%', letter_spacing=1.5)
d.append(text)
# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some text on a closed path', 10, x=x, y=y, path=path)
d.append(text)
# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 75% offset', 10, x=x, y=y, path=path, text_anchor='middle', startOffset='75%', dy=7)
d.append(text)
# text on an arbitrary path
ix = -len_x/3
iy = -len_y/4
path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/3, len_y, len_x/3, -len_y, len_x-len_x/6, len_y/2)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('it can be used for any path element, including paths that are not closed', 10, x=x, y=y, path=path, dy=7)
d.append(text)
text = draw.Text('this is just a regular text without any path', 10, x=0, y=len_y/2-10, text_anchor='middle')
d.append(text)
text = draw.Text('regular text with increased letter spacing', 10, x=0, y=-len_y/2+7, text_anchor='middle', letter_spacing=1.25)
d.append(text)
d.rasterize() |
|
@cduck if you are not available for the review perhaps we should convert this PR into a draft? |
cduck
left a comment
There was a problem hiding this comment.
Thanks for the reminder. I shouldn't have left this for so long. I really appreciate your help on the project.
I have a lot of minor suggestions but overall it looks pretty good. The main thing I noticed when trying it out is this implementation doesn't handle the center argument or multi-line text.
When center=True, I would expect it to default to text_anchor='middle', startOffset='50%', and an appropriate emOffset (only for each that isn't specified).
I checked multi-line text on a path is supported by SVG. Just make sure the tspans added on line 436 are put in the textPath tag instead of the parent text tag.
README.md
Outdated
There was a problem hiding this comment.
Try to keep the example focused on the API of Text. I would inline these values where they are used and replace them with pre-computed constants where possible.
There was a problem hiding this comment.
Got it, I will update with constants but it will do it after everything else is ready to merge, as it is now makes it possible for me to easily modify the example just in case if it will be required.
drawSvg/elements.py
Outdated
There was a problem hiding this comment.
I need to clean up this bit and remove the use of translate by removing centerOffset and using emOffset instead.
There was a problem hiding this comment.
Would you like to take care of it in this PR or leave it for another PR? Since we are updating the Text node I think it is a great chance to clean-up a bit and make such improvements.
drawSvg/elements.py
Outdated
There was a problem hiding this comment.
Related to the previous comment. Maybe delete this and just use a TSpan.
There was a problem hiding this comment.
I am not really sure how does TSpan help here. Wouldn't it create a child node in the SVG tree and break the structure I am trying to establish?
There was a problem hiding this comment.
The code does what we want but in a confusing way: "if this random attribute is set, don't write any content". If you just set encodedText to None or the empty string in __init__ I think it's more clear.
There was a problem hiding this comment.
Using TSpan when the text is a single line is optional and I'll leave the choice up to you.
There was a problem hiding this comment.
I cleaned-up a little bit. I can see how my implementation of this logic might be confusing but at same time I don't see clearly how TSpan makes is simpler. Let me know what you think of current version.
Regarding the multi-line texts on paths, still working on it.
There was a problem hiding this comment.
written a comment with few examples for multiline texts on paths
…it them and there is no need to specify them explicitly
|
@cduck please take a look at solution for multi-line texts on path I propose
example: text = draw.Text(
[
'just some text on a closed path worth with multiple',
'lines, each given own path'],
10, path=[
path,
nested_path])
d.append(text)and text = draw.Text('it can be used for any path element, including paths that are not closed\nand automatically shifts the path for consecutive lines', 10, path=path, dy=7)
d.append(text)looks good in Firefox but CairoSVG unfortunately is not capable of properly rendering it looking forward to hear your comments! |
|
Thanks a lot @cduck, I will look into that and sorry for not sharing the full source of the example, I just wanted to save up on information pollution in the thread... Unfortunately it turned out useful this time. I will investigate. |
|
@cduck I made some updates, please check at your convenience import drawSvg as draw
import math
len_x = 300
len_y = 300
d = draw.Drawing(len_x, len_y, origin='center')
# radii of the circles
R = 85
r = 65
rr = r - 10 # nested circle radius
# x, y are None when path argument provided for Text node
x = None
y = None
# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2
# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('text on a closed path with 10% offset and increased letter spacing', 10, path=path, startOffset='10%', letter_spacing=1.5)
d.append(text)
# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text(
[
'just some text on a closed path',
'with multiple lines'],
10, path=path)
d.append(text)
# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 75% offset', 10, path=path, text_anchor='middle', startOffset='75%', dy=7)
d.append(text)
# text on an arbitrary path
ix = -len_x/2
iy = -len_y/4
path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/4, len_y, len_x/3, -len_y, len_x-len_x/6, len_y/2)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('it can be used for any path element, including paths that are not closed\nand automatically shifts the path for consecutive lines', 10, path=path, dy=1)
d.append(text)
text = draw.Text('this is just a regular text without any path', 10, x=0, y=len_y/2-10, text_anchor='middle')
d.append(text)
text = draw.Text('regular text with increased letter spacing', 10, x=0, y=-len_y/2+7, text_anchor='middle', letter_spacing=1.25)
d.append(text)
d.rasterize() |
cduck
left a comment
There was a problem hiding this comment.
Getting better. The core feature works but there are some final details. Thanks for your patience.
dyshifts by pixels for single line text but by em for multi-line text.center=Truestill causes weird misalignment and doesn't center on the path.- Drawing normal text without keyword arguments text no longer works:
draw.Text('Text', 10, 0, 0)
Now that the API is mostly settled, we can also look at simplifying the example. I think your 4 or 5 examples can fit on about 20 lines of code. Each example should emphasize what one of the arguments to Text does.
If you want, I can do the final touches on the PR like making sure center works and everything is backwards compatible.
|
Thanks for your feedback @cduck and for offering to help out. I propose the following. Let me make a one more round of corrections and then you could give it a final touch and I'll simplify the example in parallel. If you agree for such plan please leave a "👍" under this comment. |
|
@cduck just finished implementing the next round of changes.
Let me know what do you think! import drawSvg as draw
import math
len_x = 300
len_y = 300
d = draw.Drawing(len_x, len_y, origin='center')
# radii of the circles
R = 85
r = 65
rr = r - 10 # nested circle radius
# x, y are None when path argument provided for Text node
x = None
y = None
# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2
# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, None, None, fx, fy)
path.A(r, r, 90, None, None, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('reversed direction text on a path with 10% offset and increased letter spacing', 10, None, None, path=path, startOffset='10%', letter_spacing=1.5)
d.append(text)
# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text(
[
'just some text on a closed path',
'with multiple lines'],
10, None, None, path=path)
d.append(text)
# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)
ix = cx-r
iy = cy
fx = cx+r
fy = cy
path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 75% offset', 10, None, None, path=path, text_anchor='middle', startOffset='75%')
d.append(text)
# text on an arbitrary path
ix = -len_x/2
iy = -len_y/4
path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/2, 1.125*len_y, len_x/6, -len_y, len_x/2, len_y/4)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('it can be used for any path element, including paths that are not closed\nand automatically shifts the path for consecutive lines', 10, None, None, path=path, dy=1, center=True)
d.append(text)
text = draw.Text('this is just a regular text without any path', 10, 0, len_y/2-10, text_anchor='middle')
d.append(text)
text = draw.Text('regular text with increased letter spacing', 10, 0, -len_y/2+7, text_anchor='middle', letter_spacing=1.25)
d.append(text)
text = draw.Text('backwards compatibility test', 10, 0, 0)
d.append(text)
d.rasterize() |
|
hi @cduck ! are we planning to merge it? Let me know if anything else is required from my side. |
|
Yes. Let me go through and merge the code this weekend. Then maybe you can write a simplified version of your example for the README. |
6e5c9fe to
e154e91
Compare
…me text to example 1 of the README
|
@marekyggdrasil I changed a lot of the code but kept the core design you implemented. I'm ready to merge it once you've taken a look. Summary of changes:
|
|
Thank you for handling the changes. I have reviewed them and I am ok with merging. I will make a new PR with example. Awesome work! |
|
Great. Thanks for all your hard work and patience on this. I just published version 1.8.1. |

















Regarding issue #21
Let me know if you like anything done differently. I modified README to add an example.