540 lines
16 KiB
C++
540 lines
16 KiB
C++
/*
|
|
* 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
|