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-clip.hh"
#include "hb-raster.h"
#include "hb-raster-paint.hh"
#include "hb-raster-svg.hh"
#include "hb-raster-svg-base.hh"
#include "hb-decycler.hh"
#include <math.h>
static inline bool
svg_transform_is_identity (const hb_svg_transform_t &t)
{
return t.xx == 1.f && t.yx == 0.f &&
t.xy == 0.f && t.yy == 1.f &&
t.dx == 0.f && t.dy == 0.f;
}
static bool
svg_parse_element_transform (hb_svg_xml_parser_t &parser,
hb_svg_transform_t *out)
{
hb_svg_style_props_t style_props;
svg_parse_style_props (parser.find_attr ("style"), &style_props);
hb_svg_str_t transform = svg_pick_attr_or_style (parser, style_props.transform, "transform");
if (!transform.len)
return false;
hb_raster_svg_parse_transform (transform, out);
return true;
}
struct hb_svg_clip_collect_context_t
{
hb_svg_defs_t *defs;
hb_svg_clip_path_def_t *clip;
const char *doc_start;
unsigned doc_len;
const OT::SVG::accelerator_t *svg_accel;
const OT::SVG::svg_doc_cache_t *doc_cache;
hb_decycler_t *use_decycler;
bool *had_alloc_failure;
};
static inline void
svg_clip_append_shape (hb_svg_clip_collect_context_t *ctx,
const hb_svg_shape_emit_data_t &shape,
const hb_svg_transform_t &transform)
{
hb_svg_clip_shape_t clip_shape;
clip_shape.shape = shape;
if (!svg_transform_is_identity (transform))
{
clip_shape.has_transform = true;
clip_shape.transform = transform;
}
ctx->defs->clip_shapes.push (clip_shape);
if (likely (!ctx->defs->clip_shapes.in_error ()))
ctx->clip->shape_count++;
else if (ctx->had_alloc_failure)
*ctx->had_alloc_failure = true;
}
static void
svg_skip_subtree (hb_svg_xml_parser_t &parser)
{
int depth = 1;
while (depth > 0)
{
hb_svg_token_type_t tok = parser.next ();
if (tok == SVG_TOKEN_EOF) break;
if (tok == SVG_TOKEN_CLOSE_TAG) depth--;
else if (tok == SVG_TOKEN_OPEN_TAG) depth++;
}
}
static inline bool
svg_resolve_element_visibility (hb_svg_xml_parser_t &parser,
bool parent_visible)
{
hb_svg_style_props_t style_props;
svg_parse_style_props (parser.find_attr ("style"), &style_props);
hb_svg_str_t display_str = svg_pick_attr_or_style (parser, style_props.display, "display");
hb_svg_str_t visibility_str = svg_pick_attr_or_style (parser, style_props.visibility, "visibility");
if (display_str.trim ().eq_ascii_ci ("none"))
return false;
hb_svg_str_t vis_trim = visibility_str.trim ();
if (!vis_trim.len || vis_trim.eq_ascii_ci ("inherit"))
return parent_visible;
if (vis_trim.eq_ascii_ci ("hidden") ||
vis_trim.eq_ascii_ci ("collapse"))
return false;
if (vis_trim.eq_ascii_ci ("visible"))
return true;
return parent_visible;
}
static void
svg_clip_collect_ref_element (hb_svg_clip_collect_context_t *ctx,
hb_svg_xml_parser_t &parser,
const hb_svg_transform_t &base_transform,
unsigned depth,
bool suppress_viewbox_once = false,
bool parent_visible = true,
bool allow_symbol_once = false);
static void
svg_clip_collect_use_target (hb_svg_clip_collect_context_t *ctx,
hb_svg_xml_parser_t &use_parser,
const hb_svg_transform_t &base_transform,
unsigned depth)
{
const unsigned SVG_MAX_CLIP_USE_DEPTH = 64;
if (depth >= SVG_MAX_CLIP_USE_DEPTH)
return;
hb_svg_str_t href = hb_raster_svg_find_href_attr (use_parser);
hb_svg_str_t ref_id;
if (!hb_raster_svg_parse_local_id_ref (href, &ref_id, nullptr))
return;
const char *found = nullptr;
if (!hb_raster_svg_find_element_by_id (ctx->doc_start, ctx->doc_len,
ctx->svg_accel, ctx->doc_cache,
ref_id, &found))
return;
hb_decycler_node_t node (*ctx->use_decycler);
if (unlikely (!node.visit ((uintptr_t) found)))
return;
hb_svg_transform_t effective = base_transform;
float use_x = 0.f, use_y = 0.f, use_w = 0.f, use_h = 0.f;
hb_raster_svg_parse_use_geometry (use_parser, &use_x, &use_y, &use_w, &use_h);
if (use_x != 0.f || use_y != 0.f)
{
hb_svg_transform_t tr;
tr.dx = use_x;
tr.dy = use_y;
effective.multiply (tr);
}
unsigned remaining = ctx->doc_len - (unsigned) (found - ctx->doc_start);
hb_svg_xml_parser_t ref_parser (found, remaining);
hb_svg_token_type_t rt = ref_parser.next ();
if (rt != SVG_TOKEN_OPEN_TAG && rt != SVG_TOKEN_SELF_CLOSE_TAG)
return;
bool viewport_mapped = false;
hb_svg_transform_t vb_t;
if (hb_raster_svg_compute_use_target_viewbox_transform (ref_parser, use_w, use_h, &vb_t))
{
effective.multiply (vb_t);
viewport_mapped = true;
}
bool allow_symbol = ref_parser.tag_name.eq ("symbol");
svg_clip_collect_ref_element (ctx, ref_parser, effective, depth + 1,
viewport_mapped, true, allow_symbol);
}
static void
svg_clip_collect_ref_element (hb_svg_clip_collect_context_t *ctx,
hb_svg_xml_parser_t &parser,
const hb_svg_transform_t &base_transform,
unsigned depth,
bool suppress_viewbox_once,
bool parent_visible,
bool allow_symbol_once)
{
const unsigned SVG_MAX_CLIP_REF_DEPTH = 64;
if (depth >= SVG_MAX_CLIP_REF_DEPTH)
{
if (!parser.self_closing)
svg_skip_subtree (parser);
return;
}
bool is_visible = svg_resolve_element_visibility (parser, parent_visible);
if (!is_visible)
{
if (!parser.self_closing)
svg_skip_subtree (parser);
return;
}
/* Definitions are not directly renderable clip geometry. */
if (parser.tag_name.eq ("defs"))
{
if (!parser.self_closing)
svg_skip_subtree (parser);
return;
}
if (parser.tag_name.eq ("symbol") && !allow_symbol_once)
{
if (!parser.self_closing)
svg_skip_subtree (parser);
return;
}
if (parser.tag_name.eq ("symbol"))
allow_symbol_once = false;
hb_svg_transform_t effective = base_transform;
hb_svg_style_props_t geom_style_props;
svg_parse_style_props (parser.find_attr ("style"), &geom_style_props);
hb_svg_transform_t local_t;
if (svg_parse_element_transform (parser, &local_t))
effective.multiply (local_t);
if (parser.tag_name.eq ("svg"))
{
float svg_x = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.x, "x"));
float svg_y = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.y, "y"));
if (svg_x != 0.f || svg_y != 0.f)
{
hb_svg_transform_t tr;
tr.dx = svg_x;
tr.dy = svg_y;
effective.multiply (tr);
}
if (!suppress_viewbox_once)
{
float vb_x = 0.f, vb_y = 0.f, vb_w = 0.f, vb_h = 0.f;
if (hb_raster_svg_parse_viewbox (parser.find_attr ("viewBox"),
&vb_x, &vb_y, &vb_w, &vb_h))
{
float viewport_w = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.width, "width"));
float viewport_h = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.height, "height"));
if (!(viewport_w > 0.f && viewport_h > 0.f))
{
viewport_w = vb_w;
viewport_h = vb_h;
}
hb_svg_transform_t vb_t;
if (hb_raster_svg_compute_viewbox_transform (viewport_w, viewport_h,
vb_x, vb_y, vb_w, vb_h,
parser.find_attr ("preserveAspectRatio"),
&vb_t))
effective.multiply (vb_t);
}
suppress_viewbox_once = false;
}
}
hb_svg_shape_emit_data_t shape;
if (hb_raster_svg_parse_shape_tag (parser, &shape))
{
svg_clip_append_shape (ctx, shape, effective);
if (!parser.self_closing)
svg_skip_subtree (parser);
return;
}
if (parser.tag_name.eq ("use"))
{
svg_clip_collect_use_target (ctx, parser, effective, depth + 1);
if (!parser.self_closing)
svg_skip_subtree (parser);
return;
}
bool is_container = hb_raster_svg_tag_is_container (parser.tag_name);
if (!is_container || parser.self_closing)
{
if (!parser.self_closing)
svg_skip_subtree (parser);
return;
}
int inner_depth = 1;
while (inner_depth > 0)
{
hb_svg_token_type_t tok = parser.next ();
if (tok == SVG_TOKEN_EOF) break;
if (tok == SVG_TOKEN_CLOSE_TAG)
{
inner_depth--;
continue;
}
if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG)
svg_clip_collect_ref_element (ctx, parser, effective, depth + 1, false, is_visible, false);
}
}
void
hb_raster_svg_process_clip_path_def (hb_svg_defs_t *defs,
hb_svg_xml_parser_t &parser,
hb_svg_token_type_t tok,
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_clip_path_def_t clip;
hb_svg_str_t id = parser.find_attr ("id");
hb_svg_str_t units = parser.find_attr ("clipPathUnits").trim ();
if (units.eq_ascii_ci ("objectBoundingBox"))
clip.units_user_space = false;
else if (units.eq_ascii_ci ("userSpaceOnUse"))
clip.units_user_space = true;
else
clip.units_user_space = true;
hb_svg_transform_t cp_t;
if (svg_parse_element_transform (parser, &cp_t))
{
clip.has_clip_transform = true;
clip.clip_transform = cp_t;
}
clip.first_shape = defs->clip_shapes.length;
clip.shape_count = 0;
if (tok == SVG_TOKEN_OPEN_TAG)
{
const unsigned SVG_MAX_CLIP_DEPTH = 64;
hb_svg_transform_t inherited[SVG_MAX_CLIP_DEPTH];
bool inherited_visibility[SVG_MAX_CLIP_DEPTH];
inherited[0] = hb_svg_transform_t ();
inherited[1] = hb_svg_transform_t ();
inherited_visibility[0] = true;
inherited_visibility[1] = true;
hb_decycler_t use_decycler;
int cdepth = 1;
bool had_alloc_failure = false;
hb_svg_clip_collect_context_t collect_ctx = {
defs, &clip,
doc_start, doc_len, svg_accel, doc_cache,
&use_decycler, &had_alloc_failure
};
while (cdepth > 0)
{
hb_svg_token_type_t ct = parser.next ();
if (ct == SVG_TOKEN_EOF) break;
if (ct == SVG_TOKEN_CLOSE_TAG) { cdepth--; continue; }
if (ct == SVG_TOKEN_OPEN_TAG || ct == SVG_TOKEN_SELF_CLOSE_TAG)
{
if (parser.tag_name.eq ("symbol"))
{
if (ct == SVG_TOKEN_OPEN_TAG)
{
int skip_depth = 1;
while (skip_depth > 0)
{
hb_svg_token_type_t st = parser.next ();
if (st == SVG_TOKEN_EOF) break;
if (st == SVG_TOKEN_CLOSE_TAG) skip_depth--;
else if (st == SVG_TOKEN_OPEN_TAG) skip_depth++;
}
}
continue;
}
if (parser.tag_name.eq ("defs"))
{
if (ct == SVG_TOKEN_OPEN_TAG)
{
int skip_depth = 1;
while (skip_depth > 0)
{
hb_svg_token_type_t st = parser.next ();
if (st == SVG_TOKEN_EOF) break;
if (st == SVG_TOKEN_CLOSE_TAG) skip_depth--;
else if (st == SVG_TOKEN_OPEN_TAG) skip_depth++;
}
}
continue;
}
bool parent_visible = (unsigned) cdepth < SVG_MAX_CLIP_DEPTH
? inherited_visibility[cdepth]
: true;
bool is_visible = svg_resolve_element_visibility (parser, parent_visible);
bool is_hidden = !is_visible;
if (is_hidden)
{
if (ct == SVG_TOKEN_OPEN_TAG)
{
int skip_depth = 1;
while (skip_depth > 0)
{
hb_svg_token_type_t st = parser.next ();
if (st == SVG_TOKEN_EOF) break;
if (st == SVG_TOKEN_CLOSE_TAG) skip_depth--;
else if (st == SVG_TOKEN_OPEN_TAG) skip_depth++;
}
}
continue;
}
hb_svg_transform_t effective = (unsigned) cdepth < SVG_MAX_CLIP_DEPTH
? inherited[cdepth]
: hb_svg_transform_t ();
hb_svg_transform_t local;
bool has_local_transform = svg_parse_element_transform (parser, &local);
if (has_local_transform)
{
effective.multiply (local);
}
if (parser.tag_name.eq ("use"))
{
svg_clip_collect_use_target (&collect_ctx, parser, effective, 0);
}
else
{
hb_svg_shape_emit_data_t shape;
if (hb_raster_svg_parse_shape_tag (parser, &shape))
svg_clip_append_shape (&collect_ctx, shape, effective);
}
if (ct == SVG_TOKEN_OPEN_TAG)
{
if ((unsigned) (cdepth + 1) < SVG_MAX_CLIP_DEPTH)
{
inherited[cdepth + 1] = effective;
inherited_visibility[cdepth + 1] = is_visible;
}
cdepth++;
}
}
}
if (had_alloc_failure)
id = {};
}
if (id.len)
(void) defs->add_clip_path (hb_bytes_t (id.data, id.len), clip);
}
struct hb_svg_clip_emit_data_t
{
const hb_svg_defs_t *defs;
const hb_svg_clip_path_def_t *clip;
hb_transform_t<> base_transform;
hb_transform_t<> bbox_transform;
bool has_bbox_transform = false;
};
static inline hb_transform_t<>
svg_to_hb_transform (const hb_svg_transform_t &t)
{
return hb_transform_t<> (t.xx, t.yx, t.xy, t.yy, t.dx, t.dy);
}
static void
svg_clip_path_emit (hb_draw_funcs_t *dfuncs,
void *draw_data,
void *user_data)
{
hb_raster_draw_t *rdr = (hb_raster_draw_t *) draw_data;
hb_svg_clip_emit_data_t *ed = (hb_svg_clip_emit_data_t *) user_data;
const hb_svg_clip_path_def_t *clip = ed->clip;
for (unsigned i = 0; i < clip->shape_count; i++)
{
const hb_svg_clip_shape_t &s = ed->defs->clip_shapes[clip->first_shape + i];
hb_transform_t<> t = ed->base_transform;
if (ed->has_bbox_transform)
t.multiply (ed->bbox_transform);
if (clip->has_clip_transform)
t.multiply (svg_to_hb_transform (clip->clip_transform));
if (s.has_transform)
t.multiply (svg_to_hb_transform (s.transform));
hb_raster_draw_set_transform (rdr, t.xx, t.yx, t.xy, t.yy, t.x0, t.y0);
hb_svg_shape_emit_data_t shape = s.shape;
hb_raster_svg_shape_path_emit (dfuncs, draw_data, &shape);
}
}
bool
hb_raster_svg_push_clip_path_ref (hb_raster_paint_t *paint,
hb_svg_defs_t *defs,
hb_svg_str_t clip_path_str,
const hb_extents_t<> *object_bbox)
{
if (clip_path_str.is_null ()) return false;
hb_svg_str_t trimmed = clip_path_str.trim ();
if (!trimmed.len || trimmed.eq_ascii_ci ("none")) return false;
hb_svg_str_t clip_id;
if (!hb_raster_svg_parse_local_id_ref (trimmed, &clip_id, nullptr))
return false;
const hb_svg_clip_path_def_t *clip = defs->find_clip_path (hb_bytes_t (clip_id.data, clip_id.len));
if (!clip) return false;
hb_svg_clip_emit_data_t ed;
ed.defs = defs;
ed.clip = clip;
ed.base_transform = paint->current_effective_transform ();
if (!clip->units_user_space)
{
if (!object_bbox || object_bbox->is_empty ())
return false;
float w = object_bbox->xmax - object_bbox->xmin;
float h = object_bbox->ymax - object_bbox->ymin;
if (!(std::isfinite (w) && std::isfinite (h)) || w <= 0.f || h <= 0.f)
return false;
ed.has_bbox_transform = true;
ed.bbox_transform = hb_transform_t<> (w, 0, 0, h, object_bbox->xmin, object_bbox->ymin);
}
hb_raster_paint_push_clip_path (paint, svg_clip_path_emit, &ed);
return true;
}
#endif /* !HB_NO_RASTER_SVG */