| /* |
| * 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 |
| */ |
| |
| #include "hb.hh" |
| |
| #include "hb-face.hh" |
| #include "hb-raster-svg.hh" |
| #include "hb-raster-svg-base.hh" |
| #include "hb-raster-svg-parse.hh" |
| #include "hb-raster-svg-context.hh" |
| #include "hb-raster-svg-defs-scan.hh" |
| #include "hb-raster-svg-gradient.hh" |
| #include "hb-raster-svg-clip.hh" |
| #include "hb-raster-svg-bbox.hh" |
| #include "hb-raster-svg-fill.hh" |
| #include "hb-raster-svg-use.hh" |
| |
| #include <assert.h> |
| #include <stdio.h> |
| |
| |
| #define SVG_MAX_DEPTH 32 |
| |
| /* |
| * 11. Element renderer — recursive SVG rendering |
| */ |
| |
| static void svg_render_element (hb_svg_render_context_t *ctx, |
| hb_svg_xml_parser_t &parser, |
| const struct hb_svg_cascade_t &inherited); |
| |
| /* Gradient def parsing lives in hb-raster-svg-gradient.* */ |
| |
| /* Clip-path defs and push helpers live in hb-raster-svg-clip.* */ |
| |
| /* Render a single shape element */ |
| static void |
| svg_render_shape (hb_svg_render_context_t *ctx, |
| hb_svg_shape_emit_data_t &shape, |
| const hb_svg_cascade_t &state, |
| hb_svg_str_t transform_str) |
| { |
| bool has_transform = transform_str.len > 0; |
| bool has_opacity = state.opacity < 1.f; |
| bool has_clip_path = false; |
| |
| if (has_opacity) |
| ctx->push_group (); |
| |
| if (has_transform) |
| { |
| hb_svg_transform_t t; |
| hb_raster_svg_parse_transform (transform_str, &t); |
| ctx->push_transform (t.xx, t.yx, t.xy, t.yy, t.dx, t.dy); |
| } |
| |
| hb_extents_t<> bbox; |
| bool has_bbox = hb_raster_svg_compute_shape_bbox (shape, &bbox); |
| has_clip_path = hb_raster_svg_push_clip_path_ref (ctx->paint, &ctx->defs, state.clip_path, |
| has_bbox ? &bbox : nullptr); |
| |
| /* Clip with shape path, then fill */ |
| hb_raster_paint_push_clip_path (ctx->paint, hb_raster_svg_shape_path_emit, &shape); |
| |
| /* Default fill is black */ |
| if (state.fill.is_null ()) |
| { |
| hb_color_t black = HB_COLOR (0, 0, 0, 255); |
| if (state.fill_opacity < 1.f) |
| black = HB_COLOR (0, 0, 0, (uint8_t) (255 * state.fill_opacity + 0.5f)); |
| ctx->paint_color (black); |
| } |
| else |
| { |
| hb_svg_fill_context_t fill_ctx = {ctx->paint, ctx->pfuncs, ctx->font, ctx->palette, &ctx->defs}; |
| hb_raster_svg_emit_fill (&fill_ctx, state.fill, state.fill_opacity, has_bbox ? &bbox : nullptr, state.color); |
| } |
| |
| ctx->pop_clip (); |
| if (has_clip_path) |
| ctx->pop_clip (); |
| |
| if (has_transform) |
| ctx->pop_transform (); |
| |
| if (has_opacity) |
| ctx->pop_group (HB_PAINT_COMPOSITE_MODE_SRC_OVER); |
| } |
| |
| static void |
| svg_render_container_element (hb_svg_render_context_t *ctx, |
| hb_svg_xml_parser_t &parser, |
| hb_svg_str_t tag, |
| bool self_closing, |
| const hb_svg_cascade_t &state, |
| hb_svg_str_t transform_str, |
| hb_svg_str_t clip_path_str) |
| { |
| bool has_transform = transform_str.len > 0; |
| bool has_opacity = state.opacity < 1.f; |
| bool has_clip = false; |
| bool has_viewbox = false; |
| float vb_x = 0, vb_y = 0, vb_w = 0, vb_h = 0; |
| |
| if (tag.eq ("svg") || tag.eq ("symbol")) |
| { |
| hb_svg_str_t viewbox_str = parser.find_attr ("viewBox"); |
| if (viewbox_str.len) |
| { |
| has_viewbox = true; |
| hb_svg_float_parser_t vb_fp (viewbox_str); |
| vb_x = vb_fp.next_float (); |
| vb_y = vb_fp.next_float (); |
| vb_w = vb_fp.next_float (); |
| vb_h = vb_fp.next_float (); |
| } |
| } |
| |
| if (has_opacity) |
| ctx->push_group (); |
| |
| if (has_transform) |
| { |
| hb_svg_transform_t t; |
| hb_raster_svg_parse_transform (transform_str, &t); |
| ctx->push_transform (t.xx, t.yx, t.xy, t.yy, t.dx, t.dy); |
| } |
| |
| if (has_viewbox && vb_w > 0 && vb_h > 0) |
| ctx->push_transform (1, 0, 0, 1, -vb_x, -vb_y); |
| |
| has_clip = hb_raster_svg_push_clip_path_ref (ctx->paint, &ctx->defs, clip_path_str, nullptr); |
| |
| if (!self_closing) |
| { |
| 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--; |
| continue; |
| } |
| |
| if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG) |
| { |
| hb_svg_str_t child_tag = parser.tag_name; |
| if (parser.tag_name.eq ("defs")) |
| { |
| if (tok != SVG_TOKEN_SELF_CLOSE_TAG) |
| { |
| hb_svg_defs_scan_context_t scan_ctx = { |
| &ctx->defs, ctx->pfuncs, ctx->paint, |
| ctx->foreground, hb_font_get_face (ctx->font), |
| ctx->palette, |
| ctx->doc_start, ctx->doc_len, |
| ctx->svg_accel, ctx->doc_cache |
| }; |
| hb_raster_svg_process_defs_element (&scan_ctx, parser); |
| } |
| continue; |
| } |
| svg_render_element (ctx, parser, state); |
| if (tok == SVG_TOKEN_OPEN_TAG && !child_tag.eq ("g") && |
| !child_tag.eq ("svg") && !child_tag.eq ("symbol") && |
| !child_tag.eq ("use")) |
| { |
| /* Skip children of non-container elements we don't handle. */ |
| 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++; |
| } |
| } |
| } |
| } |
| } |
| |
| if (has_viewbox && vb_w > 0 && vb_h > 0) |
| ctx->pop_transform (); |
| |
| if (has_clip) |
| ctx->pop_clip (); |
| |
| if (has_transform) |
| ctx->pop_transform (); |
| |
| if (has_opacity) |
| ctx->pop_group (HB_PAINT_COMPOSITE_MODE_SRC_OVER); |
| } |
| |
| static bool |
| svg_render_primitive_shape_element (hb_svg_render_context_t *ctx, |
| hb_svg_xml_parser_t &parser, |
| hb_svg_str_t tag, |
| const hb_svg_cascade_t &state, |
| hb_svg_str_t transform_str) |
| { |
| if (tag.eq ("path")) |
| { |
| hb_svg_str_t d = parser.find_attr ("d"); |
| if (d.len) |
| { |
| hb_svg_shape_emit_data_t shape; |
| shape.type = hb_svg_shape_emit_data_t::SHAPE_PATH; |
| shape.str_data = d; |
| svg_render_shape (ctx, shape, state, transform_str); |
| } |
| return true; |
| } |
| if (tag.eq ("rect")) |
| { |
| float x = svg_parse_float (parser.find_attr ("x")); |
| float y = svg_parse_float (parser.find_attr ("y")); |
| float w = svg_parse_float (parser.find_attr ("width")); |
| float h = svg_parse_float (parser.find_attr ("height")); |
| float rx = svg_parse_float (parser.find_attr ("rx")); |
| float ry = svg_parse_float (parser.find_attr ("ry")); |
| |
| if (w > 0 && h > 0) |
| { |
| hb_svg_shape_emit_data_t shape; |
| shape.type = hb_svg_shape_emit_data_t::SHAPE_RECT; |
| shape.params[0] = x; |
| shape.params[1] = y; |
| shape.params[2] = w; |
| shape.params[3] = h; |
| shape.params[4] = rx; |
| shape.params[5] = ry; |
| svg_render_shape (ctx, shape, state, transform_str); |
| } |
| return true; |
| } |
| if (tag.eq ("circle")) |
| { |
| float cx = svg_parse_float (parser.find_attr ("cx")); |
| float cy = svg_parse_float (parser.find_attr ("cy")); |
| float r = svg_parse_float (parser.find_attr ("r")); |
| |
| if (r > 0) |
| { |
| hb_svg_shape_emit_data_t shape; |
| shape.type = hb_svg_shape_emit_data_t::SHAPE_CIRCLE; |
| shape.params[0] = cx; |
| shape.params[1] = cy; |
| shape.params[2] = r; |
| svg_render_shape (ctx, shape, state, transform_str); |
| } |
| return true; |
| } |
| if (tag.eq ("ellipse")) |
| { |
| float cx = svg_parse_float (parser.find_attr ("cx")); |
| float cy = svg_parse_float (parser.find_attr ("cy")); |
| float rx = svg_parse_float (parser.find_attr ("rx")); |
| float ry = svg_parse_float (parser.find_attr ("ry")); |
| |
| if (rx > 0 && ry > 0) |
| { |
| hb_svg_shape_emit_data_t shape; |
| shape.type = hb_svg_shape_emit_data_t::SHAPE_ELLIPSE; |
| shape.params[0] = cx; |
| shape.params[1] = cy; |
| shape.params[2] = rx; |
| shape.params[3] = ry; |
| svg_render_shape (ctx, shape, state, transform_str); |
| } |
| return true; |
| } |
| if (tag.eq ("line")) |
| { |
| float x1 = svg_parse_float (parser.find_attr ("x1")); |
| float y1 = svg_parse_float (parser.find_attr ("y1")); |
| float x2 = svg_parse_float (parser.find_attr ("x2")); |
| float y2 = svg_parse_float (parser.find_attr ("y2")); |
| |
| hb_svg_shape_emit_data_t shape; |
| shape.type = hb_svg_shape_emit_data_t::SHAPE_LINE; |
| shape.params[0] = x1; |
| shape.params[1] = y1; |
| shape.params[2] = x2; |
| shape.params[3] = y2; |
| svg_render_shape (ctx, shape, state, transform_str); |
| return true; |
| } |
| if (tag.eq ("polyline")) |
| { |
| hb_svg_str_t points = parser.find_attr ("points"); |
| if (points.len) |
| { |
| hb_svg_shape_emit_data_t shape; |
| shape.type = hb_svg_shape_emit_data_t::SHAPE_POLYLINE; |
| shape.str_data = points; |
| svg_render_shape (ctx, shape, state, transform_str); |
| } |
| return true; |
| } |
| if (tag.eq ("polygon")) |
| { |
| hb_svg_str_t points = parser.find_attr ("points"); |
| if (points.len) |
| { |
| hb_svg_shape_emit_data_t shape; |
| shape.type = hb_svg_shape_emit_data_t::SHAPE_POLYGON; |
| shape.str_data = points; |
| svg_render_shape (ctx, shape, state, transform_str); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static void |
| svg_render_use_callback (void *render_user, |
| hb_svg_xml_parser_t &parser, |
| const void *state) |
| { |
| svg_render_element ((hb_svg_render_context_t *) render_user, |
| parser, |
| *(const hb_svg_cascade_t *) state); |
| } |
| |
| 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++; |
| } |
| } |
| |
| /* Render one element (may be a container or shape) */ |
| static void |
| svg_render_element (hb_svg_render_context_t *ctx, |
| hb_svg_xml_parser_t &parser, |
| const hb_svg_cascade_t &inherited) |
| { |
| if (ctx->depth >= SVG_MAX_DEPTH) return; |
| |
| const HB_UNUSED unsigned transform_depth = ctx->paint->transform_stack.length; |
| const HB_UNUSED unsigned clip_depth = ctx->paint->clip_stack.length; |
| const HB_UNUSED unsigned surface_depth = ctx->paint->surface_stack.length; |
| |
| ctx->depth++; |
| |
| hb_svg_str_t tag = parser.tag_name; |
| bool self_closing = parser.self_closing; |
| |
| /* Extract common attributes */ |
| hb_svg_str_t style = parser.find_attr ("style"); |
| hb_svg_style_props_t style_props; |
| svg_parse_style_props (style, &style_props); |
| hb_svg_str_t fill_attr = svg_pick_attr_or_style (parser, style_props.fill, "fill"); |
| hb_svg_str_t fill_opacity_str = svg_pick_attr_or_style (parser, style_props.fill_opacity, "fill-opacity"); |
| hb_svg_str_t opacity_str = svg_pick_attr_or_style (parser, style_props.opacity, "opacity"); |
| hb_svg_str_t transform_str = svg_pick_attr_or_style (parser, style_props.transform, "transform"); |
| hb_svg_str_t clip_path_attr = svg_pick_attr_or_style (parser, style_props.clip_path, "clip-path"); |
| hb_svg_str_t display_str = svg_pick_attr_or_style (parser, style_props.display, "display"); |
| hb_svg_str_t color_str = svg_pick_attr_or_style (parser, style_props.color, "color"); |
| hb_svg_str_t visibility_str = svg_pick_attr_or_style (parser, style_props.visibility, "visibility"); |
| |
| hb_svg_cascade_t state = inherited; |
| state.fill = (fill_attr.is_null () || svg_str_is_inherit (fill_attr)) ? inherited.fill : fill_attr; |
| state.fill_opacity = (fill_opacity_str.len && !svg_str_is_inherit (fill_opacity_str)) |
| ? svg_parse_float_clamped01 (fill_opacity_str) |
| : inherited.fill_opacity; |
| state.opacity = (opacity_str.len && !svg_str_is_inherit (opacity_str) && !svg_str_is_none (opacity_str)) |
| ? svg_parse_float_clamped01 (opacity_str) |
| : (svg_str_is_inherit (opacity_str) ? inherited.opacity : 1.f); |
| state.clip_path = (clip_path_attr.is_null () || svg_str_is_inherit (clip_path_attr)) |
| ? inherited.clip_path |
| : clip_path_attr; |
| if (svg_str_is_inherit (transform_str) || svg_str_is_none (transform_str)) |
| transform_str = {}; |
| state.color = inherited.color; |
| bool is_none = false; |
| if (color_str.len && !svg_str_eq_ascii_ci (color_str.trim (), "inherit")) |
| state.color = hb_raster_svg_parse_color (color_str, ctx->pfuncs, ctx->paint, |
| inherited.color, hb_font_get_face (ctx->font), |
| ctx->palette, &is_none); |
| state.visibility = inherited.visibility; |
| hb_svg_str_t visibility_trim = visibility_str.trim (); |
| if (visibility_trim.len && !svg_str_eq_ascii_ci (visibility_trim, "inherit")) |
| state.visibility = !(svg_str_eq_ascii_ci (visibility_trim, "hidden") || |
| svg_str_eq_ascii_ci (visibility_trim, "collapse")); |
| |
| if (svg_str_eq_ascii_ci (display_str.trim (), "none")) |
| { |
| if (!self_closing) |
| svg_skip_subtree (parser); |
| ctx->depth--; |
| assert (ctx->paint->transform_stack.length == transform_depth); |
| assert (ctx->paint->clip_stack.length == clip_depth); |
| assert (ctx->paint->surface_stack.length == surface_depth); |
| return; |
| } |
| if (!state.visibility) |
| { |
| if (!self_closing) |
| svg_skip_subtree (parser); |
| ctx->depth--; |
| assert (ctx->paint->transform_stack.length == transform_depth); |
| assert (ctx->paint->clip_stack.length == clip_depth); |
| assert (ctx->paint->surface_stack.length == surface_depth); |
| return; |
| } |
| |
| if (tag.eq ("g") || tag.eq ("svg") || tag.eq ("symbol")) |
| svg_render_container_element (ctx, parser, tag, self_closing, |
| state, transform_str, state.clip_path); |
| else if (svg_render_primitive_shape_element (ctx, parser, tag, state, transform_str)) |
| ; |
| else if (tag.eq ("use")) |
| { |
| hb_svg_use_context_t use_ctx = {ctx->paint, ctx->pfuncs, |
| ctx->doc_start, ctx->doc_len, |
| ctx->svg_accel, ctx->doc_cache, |
| &ctx->use_decycler}; |
| hb_raster_svg_render_use_element (&use_ctx, parser, &state, transform_str, |
| svg_render_use_callback, ctx); |
| } |
| |
| ctx->depth--; |
| |
| assert (ctx->paint->transform_stack.length == transform_depth); |
| assert (ctx->paint->clip_stack.length == clip_depth); |
| assert (ctx->paint->surface_stack.length == surface_depth); |
| } |
| |
| |
| /* |
| * 12. Entry point |
| */ |
| |
| hb_bool_t |
| hb_raster_svg_render (hb_raster_paint_t *paint, |
| hb_blob_t *blob, |
| hb_codepoint_t glyph, |
| hb_font_t *font, |
| unsigned palette, |
| hb_color_t foreground) |
| { |
| unsigned data_len; |
| const char *data = hb_blob_get_data (blob, &data_len); |
| if (!data || !data_len) return false; |
| |
| hb_face_t *face = hb_font_get_face (font); |
| const OT::SVG::svg_doc_cache_t *doc_cache = nullptr; |
| unsigned doc_index = 0; |
| hb_codepoint_t start_glyph = HB_CODEPOINT_INVALID; |
| hb_codepoint_t end_glyph = HB_CODEPOINT_INVALID; |
| |
| if (face && |
| hb_ot_color_glyph_get_svg_document_index (face, glyph, &doc_index) && |
| hb_ot_color_get_svg_document_glyph_range (face, doc_index, &start_glyph, &end_glyph)) |
| doc_cache = face->table.SVG->get_or_create_doc_cache (blob, data, data_len, |
| doc_index, start_glyph, end_glyph); |
| |
| if (doc_cache) |
| data = face->table.SVG->doc_cache_get_svg (doc_cache, &data_len); |
| |
| hb_paint_funcs_t *pfuncs = hb_raster_paint_get_funcs (); |
| |
| hb_svg_render_context_t ctx; |
| ctx.paint = paint; |
| ctx.pfuncs = pfuncs; |
| ctx.font = font; |
| ctx.palette = palette; |
| ctx.foreground = foreground; |
| ctx.doc_start = data; |
| ctx.doc_len = data_len; |
| ctx.svg_accel = face ? face->table.SVG.get () : nullptr; |
| ctx.doc_cache = doc_cache; |
| |
| hb_svg_cascade_t initial_state; |
| initial_state.color = foreground; |
| |
| hb_svg_defs_scan_context_t scan_ctx = { |
| &ctx.defs, ctx.pfuncs, ctx.paint, |
| ctx.foreground, hb_font_get_face (ctx.font), |
| ctx.palette, |
| ctx.doc_start, ctx.doc_len, |
| ctx.svg_accel, ctx.doc_cache |
| }; |
| hb_raster_svg_collect_defs (&scan_ctx, data, data_len); |
| |
| bool found_glyph = false; |
| unsigned glyph_start = 0, glyph_end = 0; |
| if (doc_cache && face->table.SVG->doc_cache_get_glyph_span (doc_cache, glyph, &glyph_start, &glyph_end)) |
| { |
| hb_svg_xml_parser_t parser (data + glyph_start, glyph_end - glyph_start); |
| hb_svg_token_type_t tok = parser.next (); |
| if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG) |
| { |
| hb_paint_push_font_transform (ctx.pfuncs, ctx.paint, font); |
| ctx.push_transform (1, 0, 0, -1, 0, 0); |
| svg_render_element (&ctx, parser, initial_state); |
| ctx.pop_transform (); |
| hb_paint_pop_transform (ctx.pfuncs, ctx.paint); |
| found_glyph = true; |
| } |
| } |
| else |
| { |
| /* Fallback for malformed/uncached docs: linear scan by glyph id. */ |
| char glyph_id_str[32]; |
| snprintf (glyph_id_str, sizeof (glyph_id_str), "glyph%u", glyph); |
| hb_svg_xml_parser_t parser (data, data_len); |
| while (true) |
| { |
| hb_svg_token_type_t tok = parser.next (); |
| if (tok == SVG_TOKEN_EOF) break; |
| |
| if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG) |
| { |
| hb_svg_str_t id = parser.find_attr ("id"); |
| if (id.len) |
| { |
| char id_buf[64]; |
| unsigned n = hb_min (id.len, (unsigned) sizeof (id_buf) - 1); |
| memcpy (id_buf, id.data, n); |
| id_buf[n] = '\0'; |
| |
| if (strcmp (id_buf, glyph_id_str) == 0) |
| { |
| hb_paint_push_font_transform (ctx.pfuncs, ctx.paint, font); |
| ctx.push_transform (1, 0, 0, -1, 0, 0); |
| svg_render_element (&ctx, parser, initial_state); |
| ctx.pop_transform (); |
| hb_paint_pop_transform (ctx.pfuncs, ctx.paint); |
| found_glyph = true; |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| return found_glyph; |
| } |