| /* |
| * Copyright © 2026 Google, Inc. |
| * |
| * 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. |
| */ |
| |
| #ifndef HB_VECTOR_OUTPUT_HH |
| #define HB_VECTOR_OUTPUT_HH |
| |
| #include <algorithm> |
| #include <math.h> |
| #include <string> |
| #include <vector> |
| |
| #include "helper-extents-overlay.hh" |
| #include "output-options.hh" |
| #include "view-options.hh" |
| #include "hb-vector.h" |
| #include "hb-ot.h" |
| |
| |
| static const char *vector_supported_formats[] = { |
| "svg", |
| "pdf", |
| nullptr |
| }; |
| |
| struct vector_output_t : output_options_t<>, view_options_t |
| { |
| static const bool repeat_shape = false; |
| |
| ~vector_output_t () |
| { |
| hb_font_destroy (font); |
| } |
| |
| void add_options (option_parser_t *parser) |
| { |
| parser->set_summary ("Draw text with given font."); |
| parser->set_description ("Shows shaped glyph outlines as SVG or PDF."); |
| |
| output_options_t::add_options (parser, vector_supported_formats); |
| view_options_t::add_options (parser); |
| |
| GOptionEntry entries[] = |
| { |
| {"precision", 0, 0, G_OPTION_ARG_INT, &this->precision, "Decimal precision (default: 2)", "N"}, |
| {nullptr} |
| }; |
| parser->add_group (entries, |
| "vector", |
| "Vector options:", |
| "Options for vector output", |
| this); |
| } |
| |
| void post_parse (GError **error) |
| { |
| view_options_t::post_parse (error); |
| if (error && *error) |
| return; |
| |
| if (precision < 0) |
| { |
| g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, |
| "precision must be non-negative"); |
| return; |
| } |
| |
| if (output_format && |
| g_ascii_strcasecmp (output_format, "svg") != 0 && |
| g_ascii_strcasecmp (output_format, "pdf") != 0) |
| { |
| char *items = g_strjoinv ("/", const_cast<char **> (vector_supported_formats)); |
| g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, |
| "Unknown output format `%s'; supported formats are: %s", |
| output_format, items); |
| g_free (items); |
| return; |
| } |
| |
| foreground = HB_COLOR (foreground_color.b, |
| foreground_color.g, |
| foreground_color.r, |
| foreground_color.a); |
| background = HB_COLOR (background_color.b, |
| background_color.g, |
| background_color.r, |
| background_color.a); |
| |
| load_custom_palette_overrides_from_view (); |
| } |
| |
| template <typename app_t> |
| void init (hb_buffer_t *buffer HB_UNUSED, const app_t *font_opts) |
| { |
| lines.clear (); |
| direction = HB_DIRECTION_INVALID; |
| |
| font = hb_font_reference (font_opts->font); |
| subpixel_bits = font_opts->subpixel_bits; |
| normalized_stroke_width = get_normalized_stroke_width (); |
| } |
| |
| void new_line () |
| { |
| lines.emplace_back (); |
| } |
| |
| void consume_text (hb_buffer_t * HB_UNUSED, |
| const char * HB_UNUSED, |
| unsigned int HB_UNUSED, |
| hb_bool_t HB_UNUSED) |
| {} |
| |
| void error (const char *message) |
| { |
| g_printerr ("%s: %s\n", g_get_prgname (), message); |
| } |
| |
| void consume_glyphs (hb_buffer_t *buffer, |
| const char * HB_UNUSED, |
| unsigned int HB_UNUSED, |
| hb_bool_t HB_UNUSED) |
| { |
| if (lines.empty ()) |
| lines.emplace_back (); |
| |
| line_t &line = lines.back (); |
| hb_direction_t line_direction = hb_buffer_get_direction (buffer); |
| if (line_direction == HB_DIRECTION_INVALID) |
| line_direction = HB_DIRECTION_LTR; |
| if (direction == HB_DIRECTION_INVALID) |
| direction = line_direction; |
| |
| unsigned int count = 0; |
| const hb_glyph_info_t *infos = hb_buffer_get_glyph_infos (buffer, &count); |
| const hb_glyph_position_t *positions = hb_buffer_get_glyph_positions (buffer, &count); |
| |
| hb_position_t pen_x = 0; |
| hb_position_t pen_y = 0; |
| |
| line.glyphs.reserve (line.glyphs.size () + count); |
| for (unsigned i = 0; i < count; i++) |
| { |
| line.glyphs.push_back ({ |
| infos[i].codepoint, |
| (float) (pen_x + positions[i].x_offset), |
| (float) (pen_y + positions[i].y_offset), |
| (float) positions[i].x_advance, |
| (float) positions[i].y_advance, |
| }); |
| |
| pen_x += positions[i].x_advance; |
| pen_y += positions[i].y_advance; |
| } |
| |
| line.advance_x = (float) pen_x; |
| line.advance_y = (float) pen_y; |
| } |
| |
| template <typename app_t> |
| void finish (hb_buffer_t *buffer HB_UNUSED, const app_t *app HB_UNUSED) |
| { |
| hb_vector_extents_t extents = {}; |
| if (!compute_extents (&extents)) |
| return; |
| final_extents = extents; |
| |
| hb_vector_format_t fmt = HB_VECTOR_FORMAT_SVG; |
| if (output_format && g_ascii_strcasecmp (output_format, "pdf") == 0) |
| fmt = HB_VECTOR_FORMAT_PDF; |
| |
| hb_vector_draw_t *draw = hb_vector_draw_create_or_fail (fmt); |
| hb_vector_paint_t *paint = hb_vector_paint_create_or_fail (fmt); |
| |
| float scale = scalbnf (1.f, (int) subpixel_bits); |
| |
| hb_vector_draw_set_scale_factor (draw, scale, scale); |
| hb_vector_draw_set_extents (draw, &extents); |
| hb_vector_draw_set_precision (draw, precision); |
| hb_vector_draw_set_foreground (draw, foreground); |
| hb_vector_draw_set_background (draw, background); |
| |
| if (paint) |
| { |
| hb_vector_paint_set_scale_factor (paint, scale, scale); |
| hb_vector_paint_set_extents (paint, &extents); |
| hb_vector_paint_set_foreground (paint, foreground); |
| hb_vector_paint_set_background (paint, background); |
| hb_vector_paint_set_palette (paint, this->palette); |
| apply_custom_palette (paint); |
| hb_vector_paint_set_precision (paint, precision); |
| } |
| |
| bool had_draw = false; |
| bool had_paint = false; |
| hb_vector_extents_mode_t extents_mode = HB_VECTOR_EXTENTS_MODE_NONE; |
| const bool use_foreground_palette = |
| foreground_use_palette && foreground_palette && foreground_palette->len; |
| unsigned palette_glyph_index = 0; |
| |
| /* Pick draw vs paint mode. --paint wins over --draw; |
| * otherwise auto-detect via the font's color tables. |
| * hb_vector_paint_glyph synthesizes paint from outlines |
| * for mono fonts, so --paint works even without color. */ |
| hb_face_t *face = hb_font_get_face (font); |
| bool font_has_color = hb_ot_color_has_paint (face) || |
| hb_ot_color_has_layers (face) || |
| hb_ot_color_has_png (face); |
| bool use_paint = force_paint ? true |
| : force_draw ? false |
| : font_has_color; |
| |
| hb_direction_t dir = direction; |
| if (dir == HB_DIRECTION_INVALID) |
| dir = HB_DIRECTION_LTR; |
| bool vertical = HB_DIRECTION_IS_VERTICAL (dir); |
| |
| float asc = 0.f, desc = 0.f, gap = 0.f; |
| get_line_metrics (dir, &asc, &desc, &gap); |
| float step = fabsf (asc - desc + gap + (float) line_space); |
| if (!(step > 0.f)) |
| step = scale; |
| line_step = step; |
| |
| for (unsigned li = 0; li < lines.size (); li++) |
| { |
| float off_x = vertical ? -(step * (float) li) : 0.f; |
| float off_y = vertical ? 0.f : -(step * (float) li); |
| |
| for (const auto &g : lines[li].glyphs) |
| { |
| float pen_x = g.x + off_x; |
| float pen_y = g.y + off_y; |
| |
| if (use_paint && paint) |
| { |
| if (use_foreground_palette) |
| { |
| const rgba_color_t &c = |
| g_array_index (foreground_palette, rgba_color_t, |
| palette_glyph_index++ % foreground_palette->len); |
| hb_vector_paint_set_foreground (paint, HB_COLOR (c.b, c.g, c.r, c.a)); |
| } |
| |
| hb_vector_paint_set_transform (paint, 1.f, 0.f, 0.f, 1.f, pen_x, pen_y); |
| hb_vector_paint_glyph (paint, font, g.gid, |
| extents_mode); |
| had_paint = true; |
| if (show_extents) |
| { |
| hb_glyph_extents_t ge; |
| if (hb_font_get_glyph_extents (font, g.gid, &ge)) |
| util_emit_extents_overlay_into_paint ( |
| hb_vector_paint_get_funcs (paint), paint, |
| &ge, pen_x, pen_y, (float) normalized_stroke_width); |
| } |
| } |
| else |
| { |
| if (use_foreground_palette) |
| { |
| const rgba_color_t &c = |
| g_array_index (foreground_palette, rgba_color_t, |
| palette_glyph_index++ % foreground_palette->len); |
| hb_vector_draw_set_foreground (draw, HB_COLOR (c.b, c.g, c.r, c.a)); |
| } |
| |
| hb_vector_draw_set_transform (draw, 1.f, 0.f, 0.f, 1.f, pen_x, pen_y); |
| hb_vector_draw_glyph (draw, font, g.gid, |
| extents_mode); |
| had_draw = true; |
| if (show_extents) |
| { |
| hb_glyph_extents_t ge; |
| if (hb_font_get_glyph_extents (font, g.gid, &ge)) |
| util_emit_extents_overlay_into_draw ( |
| hb_vector_draw_get_funcs (draw), draw, |
| &ge, pen_x, pen_y, (float) normalized_stroke_width); |
| } |
| } |
| } |
| } |
| |
| hb_blob_t *blob = had_paint ? hb_vector_paint_render (paint) |
| : had_draw ? hb_vector_draw_render (draw) |
| : nullptr; |
| if (blob && blob != hb_blob_get_empty ()) |
| write_blob (blob, !had_paint); |
| hb_blob_destroy (blob); |
| hb_vector_draw_destroy (draw); |
| hb_vector_paint_destroy (paint); |
| |
| hb_font_destroy (font); |
| font = nullptr; |
| lines.clear (); |
| } |
| |
| private: |
| |
| struct glyph_instance_t |
| { |
| hb_codepoint_t gid; |
| float x; |
| float y; |
| float ax; |
| float ay; |
| }; |
| |
| struct line_t |
| { |
| float advance_x = 0.f; |
| float advance_y = 0.f; |
| std::vector<glyph_instance_t> glyphs; |
| }; |
| |
| |
| bool compute_extents (hb_vector_extents_t *extents) |
| { |
| if (lines.empty ()) |
| { |
| extents->x = 0; |
| extents->y = 0; |
| extents->width = 1; |
| extents->height = 1; |
| return true; |
| } |
| |
| hb_direction_t dir = direction; |
| if (dir == HB_DIRECTION_INVALID) |
| dir = HB_DIRECTION_LTR; |
| bool vertical = HB_DIRECTION_IS_VERTICAL (dir); |
| |
| float scale = scalbnf (1.f, (int) subpixel_bits); |
| |
| float asc = 0.f, desc = 0.f, gap = 0.f; |
| get_line_metrics (dir, &asc, &desc, &gap); |
| float step = fabsf (asc - desc + gap + (float) line_space); |
| if (!(step > 0.f)) |
| step = scale; |
| |
| bool valid = false; |
| float x1 = 0, y1 = 0, x2 = 0, y2 = 0; |
| bool include_logical = include_logical_extents (); |
| bool include_ink = include_ink_extents (); |
| |
| auto include_point = [&] (float x, float y) |
| { |
| if (!valid) |
| { |
| x1 = x2 = x; |
| y1 = y2 = y; |
| valid = true; |
| return; |
| } |
| x1 = hb_min (x1, x); |
| y1 = hb_min (y1, y); |
| x2 = hb_max (x2, x); |
| y2 = hb_max (y2, y); |
| }; |
| |
| for (unsigned li = 0; li < lines.size (); li++) |
| { |
| float off_x = vertical ? -(step * (float) li) : 0.f; |
| float off_y = vertical ? 0.f : -(step * (float) li); |
| |
| const line_t &line = lines[li]; |
| |
| if (include_logical) |
| { |
| float log_min = hb_min (desc, asc); |
| float log_max = hb_max (desc, asc); |
| if (vertical) |
| { |
| include_point (off_x + log_min, hb_min (0.f, line.advance_y)); |
| include_point (off_x + log_max, hb_max (0.f, line.advance_y)); |
| } |
| else |
| { |
| include_point (hb_min (0.f, line.advance_x), off_y + log_min); |
| include_point (hb_max (0.f, line.advance_x), off_y + log_max); |
| } |
| } |
| |
| if (include_ink) |
| for (const auto &g : line.glyphs) |
| { |
| hb_glyph_extents_t ge; |
| if (!hb_font_get_glyph_extents (font, g.gid, &ge)) |
| continue; |
| |
| float gx = g.x + off_x; |
| float gy = g.y + off_y; |
| |
| float xa = gx + ge.x_bearing; |
| float ya = gy + ge.y_bearing; |
| float xb = xa + ge.width; |
| float yb = ya + ge.height; |
| |
| include_point (xa, ya); |
| include_point (xb, yb); |
| } |
| } |
| |
| if (!valid) |
| { |
| extents->x = 0; |
| extents->y = 0; |
| extents->width = 1; |
| extents->height = 1; |
| return true; |
| } |
| |
| if (x2 <= x1) |
| x2 = x1 + 1; |
| if (y2 <= y1) |
| y2 = y1 + 1; |
| |
| /* Margin is in pixels; extents are in font-scale (pixel * SCALE). */ |
| x1 -= (float) margin.l * scale; |
| y1 -= (float) margin.t * scale; |
| x2 += (float) margin.r * scale; |
| y2 += (float) margin.b * scale; |
| |
| extents->x = x1; |
| extents->y = y1; |
| extents->width = x2 - x1; |
| extents->height = y2 - y1; |
| return true; |
| } |
| |
| |
| double get_normalized_stroke_width () const |
| { |
| if (stroke_width <= 0.) |
| return stroke_width; |
| |
| /* Stroke width is in pixels; geometry is in font-scale |
| * (pixel * SCALE), so multiply by SCALE. */ |
| return stroke_width * scalbn (1., (int) subpixel_bits); |
| } |
| |
| void write_blob (hb_blob_t *blob, |
| hb_bool_t is_draw_blob HB_UNUSED) |
| { |
| unsigned in_len = 0; |
| const char *in_data = hb_blob_get_data (blob, &in_len); |
| if (!in_data || !in_len) |
| return; |
| fwrite (in_data, 1, in_len, out_fp); |
| } |
| |
| void apply_custom_palette (hb_vector_paint_t *paint) |
| { |
| hb_vector_paint_clear_custom_palette_colors (paint); |
| for (unsigned idx = 0; idx < custom_palette_values.size (); idx++) |
| if (idx < custom_palette_has_value.size () && custom_palette_has_value[idx]) |
| hb_vector_paint_set_custom_palette_color (paint, idx, custom_palette_values[idx]); |
| } |
| |
| void load_custom_palette_overrides_from_view () |
| { |
| custom_palette_values.clear (); |
| custom_palette_has_value.clear (); |
| if (!custom_palette_entries) |
| return; |
| |
| for (unsigned i = 0; i < custom_palette_entries->len; i++) |
| { |
| const custom_palette_entry_t &entry = |
| g_array_index (custom_palette_entries, custom_palette_entry_t, i); |
| unsigned idx = entry.index; |
| if (custom_palette_values.size () <= idx) |
| { |
| custom_palette_values.resize (idx + 1, HB_COLOR (0, 0, 0, 255)); |
| custom_palette_has_value.resize (idx + 1, false); |
| } |
| custom_palette_values[idx] = HB_COLOR (entry.color.b, |
| entry.color.g, |
| entry.color.r, |
| entry.color.a); |
| custom_palette_has_value[idx] = true; |
| } |
| } |
| |
| void get_line_metrics (hb_direction_t dir, float *asc, float *desc, float *gap) const |
| { |
| if (have_font_extents) |
| { |
| float s = scalbnf (1.f, (int) subpixel_bits); |
| if (asc) *asc = (float) font_extents.ascent * s; |
| if (desc) *desc = (float) (-font_extents.descent) * s; |
| if (gap) *gap = (float) font_extents.line_gap * s; |
| return; |
| } |
| |
| hb_font_extents_t fext = {0, 0, 0}; |
| hb_font_get_extents_for_direction (font, dir, &fext); |
| if (asc) *asc = (float) fext.ascender; |
| if (desc) *desc = (float) fext.descender; |
| if (gap) *gap = (float) fext.line_gap; |
| } |
| |
| int precision = 2; |
| hb_color_t background = HB_COLOR (255, 255, 255, 255); |
| hb_color_t foreground = HB_COLOR (0, 0, 0, 255); |
| |
| hb_font_t *font = nullptr; |
| std::vector<line_t> lines; |
| hb_direction_t direction = HB_DIRECTION_INVALID; |
| |
| unsigned subpixel_bits = 0; |
| double normalized_stroke_width = DEFAULT_STROKE_WIDTH; |
| float line_step = 0.f; |
| hb_vector_extents_t final_extents = {0, 0, 1, 1}; |
| std::vector<hb_color_t> custom_palette_values; |
| std::vector<bool> custom_palette_has_value; |
| }; |
| |
| #endif |