Skip to content

Commit 09ee5b3

Browse files
kjkclaude
andcommitted
Render SVG text on a path (fixes #6332)
MuPDF ran <textPath> as a plain <tspan>, so its text piled up at the origin. The referenced path is flattened to line segments and each glyph is placed by its distance along it, rotated to the tangent. startOffset sets the start; x on a nested tspan is a distance too. prompt: fix #6332, SVG text in textPath not rendered Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 763c7c1 commit 09ee5b3

5 files changed

Lines changed: 480 additions & 1 deletion

File tree

‎ext/mupdf/source/svg/svg-run.c‎

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
#define MAX_USE_DEPTH 100
3535

36+
typedef struct svg_text_path svg_text_path;
37+
3638
typedef struct svg_state
3739
{
3840
fz_matrix transform;
@@ -58,8 +60,141 @@ typedef struct svg_state
5860
int is_bold;
5961
int is_italic;
6062
int text_anchor;
63+
const svg_text_path *text_path; /* SumatraPDF: inside <textPath> */
6164
} svg_state;
6265

66+
/* SumatraPDF: text on a path (#6332). The referenced path flattened to line
67+
* segments; glyphs are placed by their distance along it. */
68+
typedef struct
69+
{
70+
float x0, y0, x1, y1, len, start;
71+
} svg_text_path_seg;
72+
73+
struct svg_text_path
74+
{
75+
svg_text_path_seg *segs;
76+
int n, cap;
77+
float cur_x, cur_y, sub_x, sub_y, total;
78+
};
79+
80+
static void
81+
svg_text_path_add(fz_context *ctx, svg_text_path *tp, float x, float y)
82+
{
83+
float dx = x - tp->cur_x, dy = y - tp->cur_y;
84+
float len = sqrtf(dx * dx + dy * dy);
85+
if (len > 0)
86+
{
87+
svg_text_path_seg *seg;
88+
if (tp->n == tp->cap)
89+
{
90+
int cap = tp->cap ? tp->cap * 2 : 64;
91+
tp->segs = fz_realloc_array(ctx, tp->segs, cap, svg_text_path_seg);
92+
tp->cap = cap;
93+
}
94+
seg = &tp->segs[tp->n++];
95+
seg->x0 = tp->cur_x;
96+
seg->y0 = tp->cur_y;
97+
seg->x1 = x;
98+
seg->y1 = y;
99+
seg->len = len;
100+
seg->start = tp->total;
101+
tp->total += len;
102+
}
103+
tp->cur_x = x;
104+
tp->cur_y = y;
105+
}
106+
107+
static void
108+
svg_text_path_moveto(fz_context *ctx, void *arg, float x, float y)
109+
{
110+
svg_text_path *tp = arg;
111+
tp->cur_x = tp->sub_x = x;
112+
tp->cur_y = tp->sub_y = y;
113+
}
114+
115+
static void
116+
svg_text_path_lineto(fz_context *ctx, void *arg, float x, float y)
117+
{
118+
svg_text_path_add(ctx, arg, x, y);
119+
}
120+
121+
static void
122+
svg_text_path_curveto(fz_context *ctx, void *arg, float x1, float y1, float x2, float y2, float x3, float y3)
123+
{
124+
svg_text_path *tp = arg;
125+
float x0 = tp->cur_x, y0 = tp->cur_y;
126+
int i, n = 16;
127+
for (i = 1; i <= n; i++)
128+
{
129+
float t = (float)i / n, u = 1 - t;
130+
float x = u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3;
131+
float y = u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3;
132+
svg_text_path_add(ctx, tp, x, y);
133+
}
134+
}
135+
136+
static void
137+
svg_text_path_closepath(fz_context *ctx, void *arg)
138+
{
139+
svg_text_path *tp = arg;
140+
svg_text_path_add(ctx, tp, tp->sub_x, tp->sub_y);
141+
}
142+
143+
static const fz_path_walker svg_text_path_walker = {
144+
svg_text_path_moveto,
145+
svg_text_path_lineto,
146+
svg_text_path_curveto,
147+
svg_text_path_closepath,
148+
};
149+
150+
/* point and unit tangent at distance d along the path; 0 when d is off it */
151+
static int
152+
svg_text_path_at(const svg_text_path *tp, float d, float *x, float *y, float *tx, float *ty)
153+
{
154+
int i;
155+
if (tp->n == 0 || d < 0 || d > tp->total)
156+
return 0;
157+
for (i = 0; i < tp->n; i++)
158+
{
159+
const svg_text_path_seg *seg = &tp->segs[i];
160+
if (d <= seg->start + seg->len || i == tp->n - 1)
161+
{
162+
float t = (d - seg->start) / seg->len;
163+
*x = seg->x0 + (seg->x1 - seg->x0) * t;
164+
*y = seg->y0 + (seg->y1 - seg->y0) * t;
165+
*tx = (seg->x1 - seg->x0) / seg->len;
166+
*ty = (seg->y1 - seg->y0) / seg->len;
167+
return 1;
168+
}
169+
}
170+
return 0;
171+
}
172+
173+
/* Like fz_show_string, but trm.e is the distance along the path and each
174+
* glyph is rotated to the tangent at its midpoint. */
175+
static fz_matrix
176+
svg_show_string_on_path(fz_context *ctx, fz_text *text, fz_font *user_font, fz_matrix trm, const char *s, const svg_text_path *tp)
177+
{
178+
float fs = trm.a;
179+
while (*s)
180+
{
181+
fz_font *font;
182+
int ucs, gid;
183+
float adv, w, x, y, tx, ty;
184+
s += fz_chartorune(&ucs, s);
185+
gid = fz_encode_character_with_fallback(ctx, user_font, ucs, 0, FZ_LANG_UNSET, &font);
186+
adv = gid >= 0 ? fz_advance_glyph(ctx, font, gid, 0) : 0;
187+
w = adv * fs;
188+
if (svg_text_path_at(tp, trm.e + w / 2, &x, &y, &tx, &ty))
189+
{
190+
fz_matrix gm = { fs * tx, fs * ty, fs * ty, -fs * tx, x - tx * w / 2, y - ty * w / 2 };
191+
fz_show_glyph_aux(ctx, text, font, gm, adv, gid, ucs, ucs, 0, 0, FZ_BIDI_LTR, FZ_LANG_UNSET);
192+
}
193+
trm.e += w;
194+
}
195+
return trm;
196+
}
197+
63198
static void svg_parse_common(fz_context *ctx, svg_document *doc, fz_xml *node, svg_state *state);
64199
static void svg_run_element(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, const svg_state *state);
65200

@@ -1746,7 +1881,10 @@ svg_run_text_string(fz_context *ctx, fz_device *dev, fz_matrix trm, const char *
17461881
trm.e -= (adv.e - trm.e);
17471882
}
17481883

1749-
trm = fz_show_string(ctx, text, font, trm, s, 0, 0, FZ_BIDI_LTR, FZ_LANG_UNSET);
1884+
if (state->text_path)
1885+
trm = svg_show_string_on_path(ctx, text, font, trm, s, state->text_path);
1886+
else
1887+
trm = fz_show_string(ctx, text, font, trm, s, 0, 0, FZ_BIDI_LTR, FZ_LANG_UNSET);
17501888

17511889
if (state->fill_is_set)
17521890
fz_fill_text(ctx, dev, text,
@@ -1813,6 +1951,10 @@ svg_run_text(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, c
18131951
char *y_att = fz_xml_att(root, "y");
18141952
char *dx_att = fz_xml_att(root, "dx");
18151953
char *dy_att = fz_xml_att(root, "dy");
1954+
svg_text_path tp = { 0 };
1955+
fz_path *path = NULL;
1956+
1957+
fz_var(path);
18161958

18171959
svg_begin_state(ctx, &local_state, inherit_state);
18181960
fz_try(ctx)
@@ -1824,6 +1966,26 @@ svg_run_text(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, c
18241966
trm.e = x;
18251967
trm.f = y;
18261968

1969+
/* SumatraPDF: text on a path (#6332) */
1970+
if (fz_xml_is_tag(root, "textPath"))
1971+
{
1972+
char *href_att = fz_xml_att_alt(root, "xlink:href", "href");
1973+
char *offset_att = fz_xml_att(root, "startOffset");
1974+
fz_xml *linked = (href_att && href_att[0] == '#') ? fz_tree_lookup(ctx, doc->idmap, href_att + 1) : NULL;
1975+
char *d_att = linked ? fz_xml_att(linked, "d") : NULL;
1976+
if (d_att)
1977+
{
1978+
path = fz_new_path(ctx);
1979+
svg_parse_path_data(ctx, path, d_att);
1980+
fz_walk_path(ctx, path, &svg_text_path_walker, &tp);
1981+
fz_drop_path(ctx, path);
1982+
path = NULL;
1983+
local_state.text_path = &tp;
1984+
trm.e = offset_att ? svg_parse_length(offset_att, tp.total, local_state.fontsize) : 0;
1985+
trm.f = 0;
1986+
}
1987+
}
1988+
18271989
if (x_att) trm.e = svg_parse_length(x_att, local_state.viewbox_w, local_state.fontsize);
18281990
if (y_att) trm.f = svg_parse_length(y_att, local_state.viewbox_h, local_state.fontsize);
18291991

@@ -1848,7 +2010,11 @@ svg_run_text(fz_context *ctx, fz_device *dev, svg_document *doc, fz_xml *root, c
18482010
}
18492011
}
18502012
fz_always(ctx)
2013+
{
2014+
fz_drop_path(ctx, path);
2015+
fz_free(ctx, tp.segs);
18512016
svg_end_state(ctx, &local_state);
2017+
}
18522018
fz_catch(ctx)
18532019
fz_rethrow(ctx);
18542020

@@ -2001,6 +2167,7 @@ svg_run_document(fz_context *ctx, svg_document *doc, fz_xml *root, fz_device *de
20012167
state.is_bold = 0;
20022168
state.is_italic = 0;
20032169
state.text_anchor = 0;
2170+
state.text_path = NULL;
20042171

20052172
fz_try(ctx)
20062173
{

0 commit comments

Comments
 (0)