[Tex/LaTex] TikZ: Text alignment and line breaking

line-breakingtikz-pgf

I have some troubles with text alignment along path in tikz. I made custom shape and wrote text along path inside it in the following way:

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{decorations.text}
\begin{document}
\begin{tikzpicture}

% shape
\draw (70:3) arc (70:110:3)
   -- (110:5)  arc (110:70:5)
   -- cycle;

\path[
  postaction={
    decorate,
    decoration={
      text along path,
      text align = center,
      text={Gershon Daly Bonifacy Yaw}
    }
  }
]
(105:4.5) arc (105:75:4.5)
(105:4.0) arc (105:75:4.0)
(105:3.5) arc (105:75:3.5);

\end{tikzpicture}
\end{document}

Result is pretty fine, but there are small issues. This is output without central alignment:

without alignment

and this is with central alignment:

with alignment

My issue is that I want a correct line breaking (don't break lines in the middle of the word) and alignment relative to the center line of the shape, not the path.

I know that when rendering text along path every letter is wrapping in different \hbox and turn to the correct degree separately.

Any suggestions?

UPDATE:

In this answer path is decorated only in case if text fits the path. May be there are similar approach to decorate path only with fitted text and left rest of the text to the other paths?

UPDATE 2:

I can hide text if the text don't fit path redefining left indent state as stated in mentioned answer above:

\makeatletter
  \pgfkeys{
    /pgf/decoration/omit long text/.code={%
      \expandafter\def\csname pgf@decorate@@text along path@left indent@options\expandafter\expandafter\expandafter\endcsname\expandafter\expandafter\expandafter{\csname pgf@decorate@@text along path@left indent@options\endcsname,switch if less than=\pgf@lib@dec@text@width to final}%
    },
  }
\makeatother

My idea is to not only skip typesetting text (switch to final state), but also set some flag that mean that text is not typeset, so I can decrease text length and call decoration again.

Best Answer

Following my idea (in a comment to the OP's question), I did some experiments with LuaTeX. These are the preliminary results.

The latex example

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{decorations.text}
\directlua{dofile("testing.lua")}
\begin{document}
\sffamily

% Before entering tikzpicture
\directlua{PrepareText({{105,75,4.5}, {105,75,4}, {105,75,3.5}}, 
           "Gershon Daly Bonifacy Yaw")}
\begin{tikzpicture}
\draw (70:3) arc (70:110:3)
   -- (110:5)  arc (110:70:5)
   -- cycle;
\directlua{TypeInArcs()}
\end{tikzpicture}
%
% A second example
\directlua{PrepareText({{105,75,4.5}, {105,75,4}, {105,75,3.5}}, 
"This is another longer test, which does not Fit")}
\begin{tikzpicture}
\draw (70:3) arc (70:110:3)
   -- (110:5)  arc (110:70:5)
   -- cycle;
\directlua{TypeInArcs()}
\end{tikzpicture}
%
% Third example
\directlua{PrepareText({{105,75,4.5}, {105,75,4}, {105,75,3.5}, {105,75,3}}, 
"This is another longer test, which now does Fit")}
\begin{tikzpicture}
\draw (70:2.5) arc (70:110:2.5)
   -- (110:5)  arc (110:70:5)
   -- cycle;
\directlua{TypeInArcs()}
\end{tikzpicture}

\end{document}

The result

After compiling with lualatex

Result

The lua code

Save it in file testing.lua:

function GetLinesFromBox()
        local lines,i,j,l,d,list,words
        lines = {}; j = 1;
        d=node.types();
        list = tex.box[0].head
        node.unprotect_glyphs(list)
        for l in node.traverse(list) do
            if (d[l.id]=="hlist") then
                words = {}; i = 1
                for l in node.traverse(l.head) do
                    if (d[l.id]=="glyph") then
                        words[i] = string.char(l.char)
                        i=i+1
                    end
                    if (d[l.id]=="glue") then
                        words[i] = " "
                        i=i+1
                    end
                end
                lines[j] = table.concat(words,"")
                j=j+1
            end
        end
        return lines
end

function ComputeLengthArc(start_, end_, radius)
    return (radius*(math.abs(start_-end_)*2*math.pi/360))
end

global_arcs = {}
global_text = ""

function PrepareText(arcs, text)
    local j,l
    global_arcs = arcs
    global_text = text
    tex.print("\\setbox0=\\vbox{\\centering\\parshape ", #arcs)
    for j=1,#arcs do
        l = ComputeLengthArc(arcs[j][1], arcs[j][2], arcs[j][3] )
        tex.print("0cm "..l.."cm ")
    end
    tex.print("\\noindent ".. text .. "}")
end

function TypeInArcs()
    local lines,j
    lines = GetLinesFromBox()
    for j=1,#global_arcs do
        if lines[j]~=nil then
            tex.sprint("\\path[ postaction = { decorate, decoration = {")
            tex.sprint("  text along path,")
            tex.sprint("  text align = center,")
            tex.sprint("  text={"..lines[j].."}")
            tex.sprint("} } ]")
            tex.sprint("("..global_arcs[j][1]..":"..global_arcs[j][3]..
                  ") arc ("..global_arcs[j][1]..":"..global_arcs[j][2]..":"..global_arcs[j][3]..");")
        end
    end
end

Explanations and Caveats

This is only a proof-of-concept. The algorithm is very naive. I typeset the text in TeX's box 0, but with a parshape with the appropiate lengths for each line. Then I examine the resulting nodes from lua, assuming each hbox to be a line, each glyph inside that box to be a character, and each glue to be an space. This way I "reconstruct" the string to be used for each line later, when drawing the tikz picture.

There are some issues I didn't solve (I don't know how):

  1. Ligatures in the text break the algorithm, because produce glyphs that do not have ascii equivalent. This is why I wrote "Fit" instead of "fit", to avoid the "fi" ligature.
  2. For the same reason, probably no accented chars are allowed (not tested)
  3. Apparently tikz interferes with TeX boxing mechanism, because if I try to create the box inside tikz environment, no box is produced. This is why I have to "prepare" the box before entering tikz.
  4. Probably more issues not found yet (not much tested)