MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
GlyphOutline.cpp
Go to the documentation of this file.
1#include "GlyphOutline.hpp"
2
4#include "TypeFaceFoundry.hpp"
5
7
8#include <ft2build.h>
9#include FT_FREETYPE_H
10#include FT_OUTLINE_H
11
12namespace MayaFlux::Portal::Text {
13
14namespace {
15
16 constexpr float k_ft_scale = 1.0F / 64.0F; ///< 26.6 fixed-point to pixels
17
18 struct DecomposeCtx {
19 std::vector<glm::vec2>* points;
20 std::vector<uint32_t>* contour_ends;
21 float tolerance;
22 glm::vec2 current;
23 };
24
25 // ---------------------------------------------------------------------------
26 // Recursive de Casteljau subdivision - appends points to ctx, excluding start
27 // ---------------------------------------------------------------------------
28
29 void subdivide_conic(
30 DecomposeCtx& ctx,
31 glm::vec2 p0,
32 glm::vec2 p1,
33 glm::vec2 p2,
34 float tol_sq)
35 {
36 const glm::vec2 m01 = (p0 + p1) * 0.5F;
37 const glm::vec2 m12 = (p1 + p2) * 0.5F;
38 const glm::vec2 mid = (m01 + m12) * 0.5F;
39
40 const glm::vec2 d = p2 - p0;
41 const glm::vec2 perp { -d.y, d.x };
42 const float dist_sq = (mid.x - p0.x) * perp.x + (mid.y - p0.y) * perp.y;
43
44 if (dist_sq * dist_sq <= tol_sq * (perp.x * perp.x + perp.y * perp.y)) {
45 ctx.points->push_back(p2);
46 ctx.current = p2;
47 return;
48 }
49
50 subdivide_conic(ctx, p0, m01, mid, tol_sq);
51 subdivide_conic(ctx, mid, m12, p2, tol_sq);
52 }
53
54 void subdivide_cubic(
55 DecomposeCtx& ctx,
56 glm::vec2 p0,
57 glm::vec2 p1,
58 glm::vec2 p2,
59 glm::vec2 p3,
60 float tol_sq)
61 {
62 const glm::vec2 m01 = (p0 + p1) * 0.5F;
63 const glm::vec2 m12 = (p1 + p2) * 0.5F;
64 const glm::vec2 m23 = (p2 + p3) * 0.5F;
65 const glm::vec2 m012 = (m01 + m12) * 0.5F;
66 const glm::vec2 m123 = (m12 + m23) * 0.5F;
67 const glm::vec2 mid = (m012 + m123) * 0.5F;
68
69 const glm::vec2 d = p3 - p0;
70 const glm::vec2 perp { -d.y, d.x };
71 const float denom = perp.x * perp.x + perp.y * perp.y;
72
73 auto dist_sq = [&](glm::vec2 p) {
74 const float v = (p.x - p0.x) * perp.x + (p.y - p0.y) * perp.y;
75 return v * v;
76 };
77
78 if (dist_sq(p1) <= tol_sq * denom && dist_sq(p2) <= tol_sq * denom) {
79 ctx.points->push_back(p3);
80 ctx.current = p3;
81 return;
82 }
83
84 subdivide_cubic(ctx, p0, m01, m012, mid, tol_sq);
85 subdivide_cubic(ctx, mid, m123, m23, p3, tol_sq);
86 }
87
88 // ---------------------------------------------------------------------------
89 // FT_Outline_Decompose callbacks
90 // ---------------------------------------------------------------------------
91
92 int cb_move_to(const FT_Vector* to, void* user)
93 {
94 auto* ctx = static_cast<DecomposeCtx*>(user);
95 if (!ctx->points->empty())
96 ctx->contour_ends->push_back(static_cast<uint32_t>(ctx->points->size()));
97
98 ctx->current = {
99 static_cast<float>(to->x) * k_ft_scale,
100 -static_cast<float>(to->y) * k_ft_scale
101 };
102 ctx->points->push_back(ctx->current);
103 return 0;
104 }
105
106 int cb_line_to(const FT_Vector* to, void* user)
107 {
108 auto* ctx = static_cast<DecomposeCtx*>(user);
109 ctx->current = {
110 static_cast<float>(to->x) * k_ft_scale,
111 -static_cast<float>(to->y) * k_ft_scale
112 };
113 ctx->points->push_back(ctx->current);
114 return 0;
115 }
116
117 int cb_conic_to(const FT_Vector* ctrl, const FT_Vector* to, void* user)
118 {
119 auto* ctx = static_cast<DecomposeCtx*>(user);
120 const glm::vec2 p1 {
121 static_cast<float>(ctrl->x) * k_ft_scale,
122 -static_cast<float>(ctrl->y) * k_ft_scale
123 };
124 const glm::vec2 p2 {
125 static_cast<float>(to->x) * k_ft_scale,
126 -static_cast<float>(to->y) * k_ft_scale
127 };
128 const float tol_sq = ctx->tolerance * ctx->tolerance;
129 subdivide_conic(*ctx, ctx->current, p1, p2, tol_sq);
130 return 0;
131 }
132
133 int cb_cubic_to(
134 const FT_Vector* ctrl1,
135 const FT_Vector* ctrl2,
136 const FT_Vector* to,
137 void* user)
138 {
139 auto* ctx = static_cast<DecomposeCtx*>(user);
140 const glm::vec2 p1 {
141 static_cast<float>(ctrl1->x) * k_ft_scale,
142 -static_cast<float>(ctrl1->y) * k_ft_scale
143 };
144 const glm::vec2 p2 {
145 static_cast<float>(ctrl2->x) * k_ft_scale,
146 -static_cast<float>(ctrl2->y) * k_ft_scale
147 };
148 const glm::vec2 p3 {
149 static_cast<float>(to->x) * k_ft_scale,
150 -static_cast<float>(to->y) * k_ft_scale
151 };
152 const float tol_sq = ctx->tolerance * ctx->tolerance;
153 subdivide_cubic(*ctx, ctx->current, p1, p2, p3, tol_sq);
154 return 0;
155 }
156
157 constexpr FT_Outline_Funcs k_funcs {
158 .move_to = cb_move_to,
159 .line_to = cb_line_to,
160 .conic_to = cb_conic_to,
161 .cubic_to = cb_cubic_to,
162 .shift = 0,
163 .delta = 0,
164 };
165
166} // namespace
167
169 FontFace& face,
170 uint32_t codepoint,
171 uint32_t pixel_size,
172 float tolerance)
173{
174 GlyphOutline result;
175 result.codepoint = codepoint;
176
177 if (!face.is_loaded()) {
179 "decompose_glyph: FontFace not loaded");
180 return result;
181 }
182
183 FT_Face ft = face.get_face();
184
185 if (const FT_Error err = FT_Set_Pixel_Sizes(ft, 0, pixel_size); err != 0) {
187 "decompose_glyph: FT_Set_Pixel_Sizes({}) failed: {}", pixel_size, static_cast<int>(err));
188 return result;
189 }
190
191 const FT_UInt idx = FT_Get_Char_Index(ft, static_cast<FT_ULong>(codepoint));
192 if (idx == 0) {
194 "decompose_glyph: no glyph for U+{:04X}", codepoint);
195 return result;
196 }
197
198 if (const FT_Error err = FT_Load_Glyph(ft, idx, FT_LOAD_NO_BITMAP); err != 0) {
200 "decompose_glyph: FT_Load_Glyph({}) failed: {}", idx, static_cast<int>(err));
201 return result;
202 }
203
204 result.advance_x = static_cast<int32_t>(ft->glyph->advance.x >> 6);
205
206 if (ft->glyph->format != FT_GLYPH_FORMAT_OUTLINE) {
207 return result;
208 }
209
210 DecomposeCtx ctx {
211 .points = &result.points,
212 .contour_ends = &result.contour_ends,
213 .tolerance = tolerance,
214 .current = {}
215 };
216
217 if (const FT_Error err = FT_Outline_Decompose(&ft->glyph->outline, &k_funcs, &ctx); err != 0) {
219 "decompose_glyph: FT_Outline_Decompose failed: {}", static_cast<int>(err));
220 result.points.clear();
221 result.contour_ends.clear();
222 return result;
223 }
224
225 if (!result.points.empty())
226 result.contour_ends.push_back(static_cast<uint32_t>(result.points.size()));
227
228 return result;
229}
230
231GlyphOutline decompose_glyph(uint32_t codepoint, float tolerance)
232{
234 if (!atlas) {
236 "decompose_glyph: no default font set");
237 return GlyphOutline { .codepoint = codepoint };
238 }
239
240 auto& foundry = TypeFaceFoundry::instance();
241
242 FontFace* face = foundry.get_default_face();
243 if (!face) {
245 "decompose_glyph: default face is null");
246 return GlyphOutline { .codepoint = codepoint };
247 }
248
249 return decompose_glyph(*face, codepoint, atlas->pixel_size(), tolerance);
250}
251
252} // namespace MayaFlux::Portal::Text
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
std::vector< uint32_t > * contour_ends
std::vector< glm::vec2 > * points
float tolerance
glm::vec2 current
FT_Face get_face() const
Raw FT_Face handle for use by GlyphAtlas.
Definition FontFace.hpp:57
bool is_loaded() const
Returns true after a successful load() call.
Definition FontFace.hpp:51
Owns a single FT_Face loaded from a file path.
Definition FontFace.hpp:24
GlyphAtlas * get_default_glyph_atlas() const
Return the default GlyphAtlas, or nullptr if set_default_font() has not been called successfully.
@ API
API calls from external code.
@ Portal
High-level user-facing API layer.
GlyphOutline decompose_glyph(FontFace &face, uint32_t codepoint, uint32_t pixel_size, float tolerance)
Decompose a Unicode codepoint into a tessellated polyline outline.
std::vector< glm::vec2 > points
std::vector< uint32_t > contour_ends
Vector outline for a single glyph as a flat polyline sequence.