Source code

Revision control

Copy as Markdown

Other Tools

/*
* Copyright © 2026 Behdad Esfahbod
*
* This is part of HarfBuzz, a text shaping library.
*
* Permission is hereby granted, without written agreement and without
* license or royalty fees, to use, copy, modify, and distribute this
* software and its documentation for any purpose, provided that the
* above copyright notice and the following two paragraphs appear in
* all copies of this software.
*
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
*
* Author(s): Behdad Esfahbod
*/
#ifndef HB_NO_RASTER_SVG
#include "hb.hh"
#include "hb-raster-svg-base.hh"
#include "hb-raster-svg-parse.hh"
void
svg_parse_style_props (hb_svg_str_t style, hb_svg_style_props_t *out)
{
if (style.is_null ()) return;
const char *p = style.data;
const char *end = style.data + style.len;
while (p < end)
{
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ';'))
p++;
if (p >= end) break;
const char *name_start = p;
while (p < end && *p != ':' && *p != ';')
p++;
const char *name_end = p;
while (name_end > name_start &&
(name_end[-1] == ' ' || name_end[-1] == '\t' || name_end[-1] == '\n' || name_end[-1] == '\r'))
name_end--;
if (p >= end || *p != ':')
{
while (p < end && *p != ';') p++;
continue;
}
p++; /* skip ':' */
const char *value_start = p;
while (p < end && *p != ';')
p++;
const char *value_end = p;
while (value_start < value_end &&
(*value_start == ' ' || *value_start == '\t' || *value_start == '\n' || *value_start == '\r'))
value_start++;
while (value_end > value_start &&
(value_end[-1] == ' ' || value_end[-1] == '\t' || value_end[-1] == '\n' || value_end[-1] == '\r'))
value_end--;
hb_svg_str_t prop_name = {name_start, (unsigned) (name_end - name_start)};
hb_svg_str_t prop_value = {value_start, (unsigned) (value_end - value_start)};
if (prop_name.len)
{
switch (hb_svg_ascii_lower (prop_name.data[0]))
{
case 'c':
if (prop_name.len == 9 && prop_name.eq_ascii_ci ("clip-path")) out->clip_path = prop_value;
else if (prop_name.len == 5 && prop_name.eq_ascii_ci ("color")) out->color = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("cx")) out->cx = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("cy")) out->cy = prop_value;
break;
case 'd':
if (prop_name.len == 7 && prop_name.eq_ascii_ci ("display")) out->display = prop_value;
else if (prop_name.len == 1 && prop_name.eq_ascii_ci ("d")) out->d = prop_value;
break;
case 'f':
if (prop_name.len == 4 && prop_name.eq_ascii_ci ("fill")) out->fill = prop_value;
else if (prop_name.len == 12 && prop_name.eq_ascii_ci ("fill-opacity")) out->fill_opacity = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("fx")) out->fx = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("fy")) out->fy = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("fr")) out->fr = prop_value;
break;
case 'g':
if ((prop_name.len == 13 && prop_name.eq_ascii_ci ("gradientunits")) ||
(prop_name.len == 14 && prop_name.eq_ascii_ci ("gradient-units"))) out->gradient_units = prop_value;
else if ((prop_name.len == 17 && prop_name.eq_ascii_ci ("gradienttransform")) ||
(prop_name.len == 18 && prop_name.eq_ascii_ci ("gradient-transform"))) out->gradient_transform = prop_value;
break;
case 'h':
if (prop_name.len == 6 && prop_name.eq_ascii_ci ("height")) out->height = prop_value;
break;
case 'o':
if (prop_name.len == 7 && prop_name.eq_ascii_ci ("opacity")) out->opacity = prop_value;
else if (prop_name.len == 6 && prop_name.eq_ascii_ci ("offset")) out->offset = prop_value;
break;
case 'p':
if (prop_name.len == 6 && prop_name.eq_ascii_ci ("points")) out->points = prop_value;
break;
case 'r':
if (prop_name.len == 1 && prop_name.eq_ascii_ci ("r")) out->r = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("rx")) out->rx = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("ry")) out->ry = prop_value;
break;
case 's':
if (prop_name.len == 10 && prop_name.eq_ascii_ci ("stop-color")) out->stop_color = prop_value;
else if (prop_name.len == 12 && prop_name.eq_ascii_ci ("stop-opacity")) out->stop_opacity = prop_value;
else if ((prop_name.len == 12 && prop_name.eq_ascii_ci ("spreadmethod")) ||
(prop_name.len == 13 && prop_name.eq_ascii_ci ("spread-method"))) out->spread_method = prop_value;
break;
case 't':
if (prop_name.len == 9 && prop_name.eq_ascii_ci ("transform")) out->transform = prop_value;
break;
case 'v':
if (prop_name.len == 10 && prop_name.eq_ascii_ci ("visibility")) out->visibility = prop_value;
break;
case 'w':
if (prop_name.len == 5 && prop_name.eq_ascii_ci ("width")) out->width = prop_value;
break;
case 'x':
if (prop_name.len == 1 && prop_name.eq_ascii_ci ("x")) out->x = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("x1")) out->x1 = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("x2")) out->x2 = prop_value;
break;
case 'y':
if (prop_name.len == 1 && prop_name.eq_ascii_ci ("y")) out->y = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("y1")) out->y1 = prop_value;
else if (prop_name.len == 2 && prop_name.eq_ascii_ci ("y2")) out->y2 = prop_value;
break;
default:
break;
}
}
if (p < end && *p == ';') p++;
}
}
float
svg_parse_number_or_percent (hb_svg_str_t s, bool *is_percent)
{
if (is_percent) *is_percent = false;
s = s.trim ();
if (!s.len) return 0.f;
if (s.data[s.len - 1] == '%')
{
if (is_percent) *is_percent = true;
hb_svg_str_t n = {s.data, s.len - 1};
return n.to_float () / 100.f;
}
return s.to_float ();
}
hb_svg_str_t
hb_raster_svg_find_href_attr (const hb_svg_xml_parser_t &parser)
{
hb_svg_str_t href = parser.find_attr ("href");
if (href.is_null ())
href = parser.find_attr ("xlink:href");
return href;
}
bool
hb_raster_svg_parse_id_ref (hb_svg_str_t s,
hb_svg_str_t *out_id,
hb_svg_str_t *out_tail)
{
if (out_id) *out_id = {};
if (out_tail) *out_tail = {};
s = s.trim ();
if (s.len && s.data[0] == '#')
{
hb_svg_str_t id = {s.data + 1, s.len - 1};
id = id.trim ();
if (!id.len)
return false;
if (out_id) *out_id = id;
return true;
}
if (!s.starts_with_ascii_ci ("url("))
return false;
const char *p = s.data + 4;
const char *end = s.data + s.len;
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
const char *q = p;
char quote = 0;
while (q < end)
{
char c = *q;
if (quote)
{
if (c == quote) quote = 0;
}
else
{
if (c == '"' || c == '\'') quote = c;
else if (c == ')') break;
}
q++;
}
if (q >= end || *q != ')')
return false;
hb_svg_str_t id = {(const char *) p, (unsigned) (q - p)};
id = id.trim ();
if (id.len >= 2 &&
((id.data[0] == '\'' && id.data[id.len - 1] == '\'') ||
(id.data[0] == '"' && id.data[id.len - 1] == '"')))
{
id.data++;
id.len -= 2;
}
id = id.trim ();
if (id.len && id.data[0] == '#')
{
id.data++;
id.len--;
}
id = id.trim ();
if (!id.len)
return false;
if (out_id) *out_id = id;
if (out_tail)
{
const char *tail = q + 1;
while (tail < end && (*tail == ' ' || *tail == '\t' || *tail == '\n' || *tail == '\r')) tail++;
*out_tail = {(const char *) tail, (unsigned) (end - tail)};
}
return true;
}
bool
hb_raster_svg_parse_local_id_ref (hb_svg_str_t s,
hb_svg_str_t *out_id,
hb_svg_str_t *out_tail)
{
if (out_id) *out_id = {};
if (out_tail) *out_tail = {};
s = s.trim ();
if (s.len && s.data[0] == '#')
{
hb_svg_str_t id = {s.data + 1, s.len - 1};
id = id.trim ();
if (!id.len)
return false;
if (out_id) *out_id = id;
return true;
}
if (!s.starts_with_ascii_ci ("url("))
return false;
hb_svg_str_t id;
if (!hb_raster_svg_parse_id_ref (s, &id, out_tail))
return false;
const char *p = s.data + 4;
const char *end = s.data + s.len;
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
if (p < end && (*p == '\'' || *p == '"'))
{
p++;
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
if (p >= end || *p != '#')
return false;
}
else if (p >= end || *p != '#')
return false;
if (out_id) *out_id = id;
return true;
}
bool
hb_raster_svg_find_element_by_id (const char *doc_start,
unsigned doc_len,
const OT::SVG::accelerator_t *svg_accel,
const OT::SVG::svg_doc_cache_t *doc_cache,
hb_svg_str_t id,
const char **found)
{
*found = nullptr;
if (!doc_start || !doc_len || !id.len)
return false;
if (doc_cache && svg_accel)
{
unsigned start = 0, end = 0;
OT::SVG::svg_id_span_t key = {id.data, id.len};
if (svg_accel->doc_cache_find_id_span (doc_cache, key, &start, &end))
{
if (start < doc_len && end <= doc_len && start < end)
{
*found = doc_start + start;
return true;
}
}
}
hb_svg_xml_parser_t search (doc_start, doc_len);
while (true)
{
hb_svg_token_type_t tok = search.next ();
if (tok == SVG_TOKEN_EOF) break;
if (tok != SVG_TOKEN_OPEN_TAG && tok != SVG_TOKEN_SELF_CLOSE_TAG) continue;
hb_svg_str_t attr_id = search.find_attr ("id");
if (attr_id.len == id.len && 0 == memcmp (attr_id.data, id.data, id.len))
{
*found = search.tag_start;
return true;
}
}
return false;
}
bool
hb_raster_svg_parse_viewbox (hb_svg_str_t viewbox_str,
float *x,
float *y,
float *w,
float *h)
{
if (!viewbox_str.len)
return false;
hb_svg_float_parser_t vb_fp (viewbox_str);
float vb_x = vb_fp.next_float ();
float vb_y = vb_fp.next_float ();
float vb_w = vb_fp.next_float ();
float vb_h = vb_fp.next_float ();
if (vb_w <= 0.f || vb_h <= 0.f)
return false;
if (x) *x = vb_x;
if (y) *y = vb_y;
if (w) *w = vb_w;
if (h) *h = vb_h;
return true;
}
static inline float
svg_align_offset (hb_svg_str_t align,
float leftover,
char axis)
{
if (leftover <= 0.f) return 0.f;
if ((axis == 'x' && align.starts_with_ascii_ci ("xMin")) ||
(axis == 'y' && (align.eq_ascii_ci ("xMinYMin") ||
align.eq_ascii_ci ("xMidYMin") ||
align.eq_ascii_ci ("xMaxYMin"))))
return 0.f;
if ((axis == 'x' && align.starts_with_ascii_ci ("xMax")) ||
(axis == 'y' && (align.eq_ascii_ci ("xMinYMax") ||
align.eq_ascii_ci ("xMidYMax") ||
align.eq_ascii_ci ("xMaxYMax"))))
return leftover;
return leftover * 0.5f;
}
bool
hb_raster_svg_compute_viewbox_transform (float viewport_w,
float viewport_h,
float vb_x,
float vb_y,
float vb_w,
float vb_h,
hb_svg_str_t preserve_aspect_ratio,
hb_svg_transform_t *out)
{
if (!(viewport_w > 0.f && viewport_h > 0.f && vb_w > 0.f && vb_h > 0.f))
return false;
hb_svg_str_t par = preserve_aspect_ratio.trim ();
if (par.starts_with_ascii_ci ("defer"))
{
const char *p = par.data + 5;
const char *end = par.data + par.len;
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
par = hb_svg_str_t (p, (unsigned) (end - p));
}
if (!par.len)
par = hb_svg_str_t ("xMidYMid meet", 12);
bool is_none = false;
bool is_slice = false;
hb_svg_str_t align = par;
const char *p = par.data;
const char *end = par.data + par.len;
while (p < end && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
align = hb_svg_str_t (par.data, (unsigned) (p - par.data)).trim ();
if (par.starts_with_ascii_ci ("none"))
is_none = true;
else if (align.starts_with_ascii_ci ("x"))
{
const char *mode = p;
while (mode < end && (*mode == ' ' || *mode == '\t' || *mode == '\n' || *mode == '\r')) mode++;
if (mode < end && hb_svg_str_t (mode, (unsigned) (end - mode)).starts_with_ascii_ci ("slice"))
is_slice = true;
}
hb_svg_transform_t t;
if (is_none)
{
t.xx = viewport_w / vb_w;
t.yy = viewport_h / vb_h;
t.dx = -vb_x * t.xx;
t.dy = -vb_y * t.yy;
*out = t;
return true;
}
float sx = viewport_w / vb_w;
float sy = viewport_h / vb_h;
float s = is_slice ? hb_max (sx, sy) : hb_min (sx, sy);
float scaled_w = vb_w * s;
float scaled_h = vb_h * s;
float leftover_x = viewport_w - scaled_w;
float leftover_y = viewport_h - scaled_h;
if (!align.starts_with_ascii_ci ("x"))
align = hb_svg_str_t ("xMidYMid", 8);
t.xx = s;
t.yy = s;
t.dx = svg_align_offset (align, leftover_x, 'x') - vb_x * s;
t.dy = svg_align_offset (align, leftover_y, 'y') - vb_y * s;
*out = t;
return true;
}
bool
hb_raster_svg_compute_use_target_viewbox_transform (hb_svg_xml_parser_t &target_parser,
float use_w,
float use_h,
hb_svg_transform_t *out)
{
if (!(target_parser.tag_name.eq ("svg") || target_parser.tag_name.eq ("symbol")))
return false;
float viewport_w = use_w;
float viewport_h = use_h;
hb_svg_style_props_t target_style_props;
svg_parse_style_props (target_parser.find_attr ("style"), &target_style_props);
if (viewport_w <= 0.f)
viewport_w = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (target_parser, target_style_props.width, "width"));
if (viewport_h <= 0.f)
viewport_h = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (target_parser, target_style_props.height, "height"));
float vb_x = 0.f, vb_y = 0.f, vb_w = 0.f, vb_h = 0.f;
if (!hb_raster_svg_parse_viewbox (target_parser.find_attr ("viewBox"), &vb_x, &vb_y, &vb_w, &vb_h))
return false;
if (!(viewport_w > 0.f && viewport_h > 0.f))
{
viewport_w = vb_w;
viewport_h = vb_h;
}
return hb_raster_svg_compute_viewbox_transform (viewport_w, viewport_h, vb_x, vb_y, vb_w, vb_h,
target_parser.find_attr ("preserveAspectRatio"),
out);
}
void
hb_raster_svg_parse_use_geometry (hb_svg_xml_parser_t &parser,
float *x,
float *y,
float *w,
float *h)
{
hb_svg_style_props_t style_props;
svg_parse_style_props (parser.find_attr ("style"), &style_props);
if (x) *x = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, style_props.x, "x"));
if (y) *y = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, style_props.y, "y"));
if (w) *w = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, style_props.width, "width"));
if (h) *h = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, style_props.height, "height"));
}
float
hb_raster_svg_parse_non_percent_length (hb_svg_str_t s)
{
bool is_percent = false;
float v = svg_parse_number_or_percent (s, &is_percent);
return is_percent ? 0.f : v;
}
#endif /* !HB_NO_RASTER_SVG */