| /* |
| * 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 */ |