| /* |
| * 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 "OT/Color/svg/svg.hh" |
| |
| #include <assert.h> |
| #include <stdio.h> |
| |
| #ifndef HB_NO_RASTER_SVG |
| |
| #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; |
| bool pushed_transform = false; |
| bool pushed_opacity_group = false; |
| |
| if (has_opacity) |
| pushed_opacity_group = ctx->push_group (); |
| |
| if (has_transform) |
| { |
| hb_svg_transform_t t; |
| hb_raster_svg_parse_transform (transform_str, &t); |
| pushed_transform = 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 (pushed_transform) |
| ctx->pop_transform (); |
| |
| if (pushed_opacity_group) |
| 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; |
| bool has_viewbox_transform = false; |
| bool has_svg_translate = false; |
| float svg_x = 0.f, svg_y = 0.f; |
| float viewport_w = 0.f, viewport_h = 0.f; |
| hb_svg_transform_t viewbox_t; |
| float vb_x = 0, vb_y = 0, vb_w = 0, vb_h = 0; |
| bool pushed_opacity_group = false; |
| bool pushed_transform = false; |
| bool pushed_svg_translate = false; |
| bool pushed_viewbox_transform = false; |
| |
| if (tag.eq ("svg") || tag.eq ("symbol")) |
| { |
| hb_svg_style_props_t geom_style_props; |
| svg_parse_style_props (parser.find_attr ("style"), &geom_style_props); |
| |
| if (tag.eq ("svg")) |
| { |
| svg_x = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.x, "x")); |
| svg_y = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.y, "y")); |
| has_svg_translate = (svg_x != 0.f || svg_y != 0.f); |
| } |
| |
| hb_svg_str_t viewbox_str = parser.find_attr ("viewBox"); |
| if (hb_raster_svg_parse_viewbox (viewbox_str, &vb_x, &vb_y, &vb_w, &vb_h)) |
| { |
| has_viewbox = true; |
| |
| if (tag.eq ("svg")) |
| { |
| viewport_w = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.width, "width")); |
| 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)) |
| { |
| if (ctx->depth == 1) |
| { |
| unsigned upem = hb_font_get_face (ctx->font)->get_upem (); |
| viewport_w = (float) upem; |
| viewport_h = (float) upem; |
| } |
| else |
| { |
| viewport_w = vb_w; |
| viewport_h = vb_h; |
| } |
| } |
| has_viewbox_transform = |
| hb_raster_svg_compute_viewbox_transform (viewport_w, viewport_h, |
| vb_x, vb_y, vb_w, vb_h, |
| parser.find_attr ("preserveAspectRatio"), |
| &viewbox_t); |
| } |
| } |
| } |
| |
| if (ctx->suppress_viewbox_once) |
| { |
| has_viewbox = false; |
| has_viewbox_transform = false; |
| ctx->suppress_viewbox_once = false; |
| } |
| |
| if (has_opacity) |
| pushed_opacity_group = ctx->push_group (); |
| |
| if (has_transform) |
| { |
| hb_svg_transform_t t; |
| hb_raster_svg_parse_transform (transform_str, &t); |
| pushed_transform = ctx->push_transform (t.xx, t.yx, t.xy, t.yy, t.dx, t.dy); |
| } |
| |
| if (has_svg_translate) |
| pushed_svg_translate = ctx->push_transform (1, 0, 0, 1, svg_x, svg_y); |
| |
| if (has_viewbox_transform) |
| pushed_viewbox_transform = ctx->push_transform (viewbox_t.xx, viewbox_t.yx, viewbox_t.xy, viewbox_t.yy, |
| viewbox_t.dx, viewbox_t.dy); |
| else if (has_viewbox && vb_w > 0 && vb_h > 0) |
| pushed_viewbox_transform = 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 && |
| !hb_raster_svg_tag_is_container_or_use (child_tag)) |
| { |
| /* 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 (pushed_viewbox_transform) |
| ctx->pop_transform (); |
| |
| if (pushed_svg_translate) |
| ctx->pop_transform (); |
| |
| if (has_clip) |
| ctx->pop_clip (); |
| |
| if (pushed_transform) |
| ctx->pop_transform (); |
| |
| if (pushed_opacity_group) |
| 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, |
| const hb_svg_cascade_t &state, |
| hb_svg_str_t transform_str) |
| { |
| hb_svg_str_t tag = parser.tag_name; |
| if (!(tag.eq ("path") || tag.eq ("rect") || tag.eq ("circle") || |
| tag.eq ("ellipse") || tag.eq ("line") || tag.eq ("polyline") || |
| tag.eq ("polygon"))) |
| return false; |
| |
| hb_svg_shape_emit_data_t shape; |
| if (hb_raster_svg_parse_shape_tag (parser, &shape)) |
| svg_render_shape (ctx, shape, state, transform_str); |
| return true; |
| } |
| |
| static void |
| svg_render_use_callback (void *render_user, |
| hb_svg_xml_parser_t &parser, |
| const void *state, |
| bool viewport_mapped) |
| { |
| hb_svg_render_context_t *ctx = (hb_svg_render_context_t *) render_user; |
| bool old_suppress = ctx->suppress_viewbox_once; |
| bool old_allow_symbol = ctx->allow_symbol_render_once; |
| if (viewport_mapped) |
| ctx->suppress_viewbox_once = true; |
| if (parser.tag_name.eq ("symbol")) |
| ctx->allow_symbol_render_once = true; |
| svg_render_element (ctx, parser, *(const hb_svg_cascade_t *) state); |
| ctx->suppress_viewbox_once = old_suppress; |
| ctx->allow_symbol_render_once = old_allow_symbol; |
| } |
| |
| /* 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 && !color_str.trim ().eq_ascii_ci ("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 && !visibility_trim.eq_ascii_ci ("inherit")) |
| state.visibility = !(visibility_trim.eq_ascii_ci ("hidden") || |
| visibility_trim.eq_ascii_ci ("collapse")); |
| |
| if (display_str.trim ().eq_ascii_ci ("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 ("symbol") && !ctx->allow_symbol_render_once) |
| { |
| 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 ("symbol")) |
| ctx->allow_symbol_render_once = false; |
| |
| if (hb_raster_svg_tag_is_container (tag)) |
| svg_render_container_element (ctx, parser, tag, self_closing, |
| state, transform_str, state.clip_path); |
| else if (svg_render_primitive_shape_element (ctx, parser, 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) |
| { |
| bool ret = false; |
| hb_blob_t *render_blob = nullptr; |
| hb_face_t *face HB_UNUSED = hb_font_get_face (font); |
| const OT::SVG::svg_doc_cache_t *doc_cache = nullptr; |
| #ifndef HB_NO_SVG |
| unsigned doc_index = 0; |
| hb_codepoint_t start_glyph = HB_CODEPOINT_INVALID; |
| hb_codepoint_t end_glyph = HB_CODEPOINT_INVALID; |
| #endif |
| hb_paint_funcs_t *pfuncs = hb_raster_paint_get_funcs (); |
| hb_svg_render_context_t ctx; |
| hb_svg_cascade_t initial_state; |
| hb_svg_defs_scan_context_t scan_ctx; |
| bool found_glyph = false; |
| #ifndef HB_NO_SVG |
| unsigned glyph_start = 0, glyph_end = 0; |
| #endif |
| |
| unsigned data_len = 0; |
| const char *data = nullptr; |
| render_blob = OT::hb_ot_svg_reference_normalized_blob (blob, &data, &data_len); |
| if (!render_blob || !data_len) |
| goto done; |
| |
| #ifndef HB_NO_SVG |
| 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 (render_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); |
| #endif |
| |
| ctx.paint = paint; |
| ctx.pfuncs = pfuncs; |
| ctx.font = font; |
| ctx.palette = palette; |
| ctx.foreground = foreground; |
| ctx.doc_start = data; |
| ctx.doc_len = data_len; |
| #ifndef HB_NO_SVG |
| ctx.svg_accel = face ? face->table.SVG.get () : nullptr; |
| #endif |
| ctx.doc_cache = doc_cache; |
| |
| initial_state.color = foreground; |
| |
| 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); |
| |
| #ifndef HB_NO_SVG |
| 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) |
| { |
| bool pushed_font_transform = hb_raster_svg_push_font_transform (ctx.pfuncs, ctx.paint, font); |
| bool pushed_flip_transform = ctx.push_transform (1, 0, 0, -1, 0, 0); |
| svg_render_element (&ctx, parser, initial_state); |
| if (pushed_flip_transform) |
| ctx.pop_transform (); |
| if (pushed_font_transform) |
| hb_paint_pop_transform (ctx.pfuncs, ctx.paint); |
| found_glyph = true; |
| } |
| } |
| #endif |
| if (!found_glyph) |
| { |
| /* Fallback for malformed/uncached docs: linear scan by glyph id. */ |
| char glyph_id_str[32]; |
| int glyph_id_len = snprintf (glyph_id_str, sizeof (glyph_id_str), "glyph%u", glyph); |
| if (glyph_id_len <= 0 || (unsigned) glyph_id_len >= sizeof (glyph_id_str)) |
| return false; |
| 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) |
| { |
| if (id.len == (unsigned) glyph_id_len && |
| 0 == hb_memcmp (id.data, glyph_id_str, (unsigned) glyph_id_len)) |
| { |
| bool pushed_font_transform = hb_raster_svg_push_font_transform (ctx.pfuncs, ctx.paint, font); |
| bool pushed_flip_transform = ctx.push_transform (1, 0, 0, -1, 0, 0); |
| svg_render_element (&ctx, parser, initial_state); |
| if (pushed_flip_transform) |
| ctx.pop_transform (); |
| if (pushed_font_transform) |
| hb_paint_pop_transform (ctx.pfuncs, ctx.paint); |
| found_glyph = true; |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| ret = found_glyph; |
| |
| done: |
| hb_blob_destroy (render_blob); |
| return ret; |
| } |
| |
| #endif /* !HB_NO_RASTER_SVG */ |