1use alpha2_media::EmojiPolicy;
4use alpha2_model::{Paint, Rgba, Spread, TextAlign, TextShadow, TextStroke, TextStyle};
5use alpha2_text::{
6 TextFill, TextGradient, TextGradientKind, TextGradientStop, TextRasterError, TextRasterRequest,
7 TextRasterizer, TextSpread,
8};
9
10use crate::{Baselines, CanvasRect, PictureSize};
11
12pub const MAX_FONT_PIXELS: f32 = 2_048.0;
14pub const MAX_STROKE_PIXELS: f32 = 64.0;
16pub const MAX_PADDING_PIXELS: f32 = 256.0;
18
19#[derive(Copy, Clone, Debug, PartialEq)]
23pub struct TextBounds {
24 pub layout: CanvasRect,
27 pub content: CanvasRect,
30 pub ink: Option<CanvasRect>,
34 pub line_count: u32,
36 pub baselines: Baselines,
38}
39
40#[must_use]
43pub fn layout_text_key(
44 request: &TextRasterRequest,
45 shadows: &[ShadowPixels],
46 animators: &[alpha2_eval::SampledAnimator],
47) -> u64 {
48 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
49 let mut feed = |bytes: &[u8]| {
50 for byte in bytes {
51 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
52 }
53 };
54 feed(&[0x4c]);
56 feed_request(&mut feed, request);
57 if !shadows.is_empty() {
58 feed(&[0xfd]);
59 feed_count(&mut feed, shadows.len());
60 for shadow in shadows {
61 for value in [shadow.offset[0], shadow.offset[1], shadow.sigma] {
62 feed(&value.to_bits().to_le_bytes());
63 }
64 for value in shadow.color {
65 feed(&value.to_bits().to_le_bytes());
66 }
67 }
68 }
69 if !animators.is_empty() {
70 let mut stable = alpha2_geometry::StableHash::new();
71 alpha2_eval::hash_sampled_animators(&mut stable, animators);
72 feed(&[0xfa]);
73 feed(&stable.finish().to_le_bytes());
74 }
75 hash.max(1)
76}
77
78#[must_use]
81pub fn shaped_key(request: &TextRasterRequest) -> u64 {
82 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
83 let mut feed = |bytes: &[u8]| {
84 for byte in bytes {
85 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
86 }
87 };
88 feed(request.text.as_bytes());
89 feed(&[0xff]);
90 feed(
91 request
92 .font_family
93 .as_deref()
94 .unwrap_or_default()
95 .as_bytes(),
96 );
97 feed(&[0xff, u8::from(request.font_family.is_some())]);
98 feed(&request.font_size.to_bits().to_le_bytes());
99 feed(&request.line_height.to_bits().to_le_bytes());
100 feed(&request.letter_spacing.to_bits().to_le_bytes());
101 feed(&request.font_weight.to_le_bytes());
102 hash
103}
104
105fn feed_request(feed: &mut impl FnMut(&[u8]), request: &TextRasterRequest) {
106 feed(request.text.as_bytes());
107 feed(&[0xff]);
108 feed(
109 request
110 .font_family
111 .as_deref()
112 .unwrap_or_default()
113 .as_bytes(),
114 );
115 feed(&[0xff, u8::from(request.font_family.is_some())]);
116 for value in [
117 request.font_size,
118 request.max_width,
119 request.line_height,
120 request.letter_spacing,
121 ] {
122 feed(&value.to_bits().to_le_bytes());
123 }
124 feed(&request.font_weight.to_le_bytes());
125 match &request.fill {
126 TextFill::Solid(color) => feed(color),
127 TextFill::Gradient(gradient) => feed_text_gradient(feed, gradient),
128 }
129 feed(&[match request.align {
130 alpha2_media::TextAlign::Start => 0,
131 alpha2_media::TextAlign::Center => 1,
132 alpha2_media::TextAlign::End => 2,
133 }]);
134 feed_count(feed, request.strokes.len());
135 for stroke in &request.strokes {
136 feed(&stroke.color);
137 feed(&stroke.width.to_bits().to_le_bytes());
138 }
139 if request.split_background {
140 feed(&[0xfd]);
141 }
142}
143
144#[allow(clippy::cast_possible_truncation)]
147fn feed_count(feed: &mut impl FnMut(&[u8]), count: usize) {
148 feed(&[count.min(255) as u8]);
149}
150
151fn feed_text_gradient(feed: &mut impl FnMut(&[u8]), gradient: &TextGradient) {
152 feed(&[0xfe]);
154 let points: &[f32] = match &gradient.kind {
155 TextGradientKind::Linear { start, end } => &[0.0, start[0], start[1], end[0], end[1]],
156 TextGradientKind::Radial { center, radius } => &[1.0, center[0], center[1], *radius],
157 TextGradientKind::Conic { center, angle } => &[2.0, center[0], center[1], *angle],
158 };
159 for value in points {
160 feed(&value.to_bits().to_le_bytes());
161 }
162 feed(&[gradient.spread as u8]);
163 feed_count(feed, gradient.stops.len());
164 for stop in &gradient.stops {
165 feed(&stop.offset.to_bits().to_le_bytes());
166 for value in stop.color {
167 feed(&value.to_bits().to_le_bytes());
168 }
169 }
170}
171
172fn feed_paint(feed: &mut impl FnMut(&[u8]), paint: &Paint) {
174 match paint {
175 Paint::Solid { color } => feed(&rgba8(*color)),
176 gradient => feed_text_gradient(feed, &text_gradient(gradient)),
177 }
178}
179
180fn feed_look(feed: &mut impl FnMut(&[u8]), strokes: &[TextStroke], shadows: &[TextShadow]) {
183 feed_count(feed, strokes.len());
184 for stroke in strokes {
185 feed(&rgba8(stroke.color));
186 feed(&stroke.width.to_bits().to_le_bytes());
187 }
188 if !shadows.is_empty() {
189 feed(&[0xfd]);
190 feed_count(feed, shadows.len());
191 for shadow in shadows {
192 feed(&rgba8(shadow.color));
193 for value in [shadow.offset.x, shadow.offset.y, shadow.blur] {
194 feed(&value.to_bits().to_le_bytes());
195 }
196 }
197 }
198}
199
200#[must_use]
203pub fn text_key(text: &str, style: &TextStyle, size: PictureSize) -> u64 {
204 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
205 let mut feed = |bytes: &[u8]| {
206 for byte in bytes {
207 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
208 }
209 };
210 feed(text.as_bytes());
211 feed(&[0xff]);
212 feed(style.font_family.as_deref().unwrap_or_default().as_bytes());
214 feed(&[0xff, u8::from(style.font_family.is_some())]);
215 for value in [
216 style.font_size,
217 style.line_height,
218 style.letter_spacing,
219 style.max_width,
220 style.position.x,
221 style.position.y,
222 style.anchor.x,
223 style.anchor.y,
224 ] {
225 feed(&value.to_bits().to_le_bytes());
226 }
227 feed(&style.font_weight.to_le_bytes());
228 feed_paint(&mut feed, &style.fill);
229 feed(&[match style.align {
230 TextAlign::Start => 0,
231 TextAlign::Center => 1,
232 TextAlign::End => 2,
233 }]);
234 feed_look(&mut feed, &style.strokes, &style.shadows);
235 match style.background {
236 Some(background) => {
237 feed(&[1]);
238 feed(&rgba8(background.color));
239 feed(&background.padding.to_bits().to_le_bytes());
240 }
241 None => feed(&[0]),
242 }
243 feed(&size.width().to_le_bytes());
244 feed(&size.height().to_le_bytes());
245 hash.max(1)
246}
247
248#[allow(clippy::cast_precision_loss)]
257pub fn measure_text(
258 rasterizer: &TextRasterizer,
259 text: &str,
260 style: &TextStyle,
261 size: PictureSize,
262) -> Result<TextBounds, TextRasterError> {
263 let request = text_request(text, style, size, text_key(text, style, size));
264 let measure = rasterizer.measure(&request)?;
265 let (width, height) = (size.width() as f32, size.height() as f32);
266 let margin = measure.decoration_margin as f32;
267 let (box_width, box_height) = (measure.box_width as f32, measure.box_height as f32);
268 let origin = [
271 style.position.x * width - style.anchor.x * (box_width + 2.0 * margin) + margin,
272 style.position.y * height - style.anchor.y * (box_height + 2.0 * margin) + margin,
273 ];
274 let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
275 left: (origin[0] + left) / width,
276 top: (origin[1] + top) / height,
277 right: (origin[0] + right) / width,
278 bottom: (origin[1] + bottom) / height,
279 };
280 Ok(TextBounds {
281 layout: rect([-margin, -margin, box_width + margin, box_height + margin]),
282 content: rect(measure.content),
283 ink: measure.ink.map(rect),
284 line_count: measure.line_count,
285 baselines: Baselines {
286 first: (origin[1] + measure.first_baseline) / height,
287 last: (origin[1] + measure.last_baseline) / height,
288 },
289 })
290}
291
292#[must_use]
294#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
295pub fn rgba8(color: Rgba) -> [u8; 4] {
296 [color.red, color.green, color.blue, color.alpha]
297 .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
298}
299
300#[must_use]
303#[allow(clippy::cast_precision_loss)]
304pub fn text_request(
305 text: &str,
306 style: &TextStyle,
307 size: PictureSize,
308 key: u64,
309) -> TextRasterRequest {
310 let font_size = font_pixels(style, size);
311 TextRasterRequest {
312 text: text.to_owned(),
313 font_size,
314 max_width: (style.max_width * size.width() as f32).max(1.0),
315 line_height: style.line_height,
316 align: match style.align {
317 TextAlign::Start => alpha2_media::TextAlign::Start,
318 TextAlign::Center => alpha2_media::TextAlign::Center,
319 TextAlign::End => alpha2_media::TextAlign::End,
320 },
321 fill: text_fill(&style.fill),
322 emoji_policy: EmojiPolicy::Color,
323 content_version: key,
324 strokes: stroke_pixels(&style.strokes, font_size),
325 split_background: !style.shadows.is_empty() && style.background.is_some(),
326 background: style
327 .background
328 .map(|background| alpha2_text::TextBackground {
329 color: rgba8(background.color),
330 padding: (background.padding * font_size).clamp(0.0, MAX_PADDING_PIXELS),
331 }),
332 font_family: style.font_family.clone(),
333 letter_spacing: style.letter_spacing * font_size,
334 font_weight: style.font_weight,
335 }
336}
337
338#[must_use]
341pub fn text_fill(paint: &Paint) -> TextFill {
342 match paint {
343 Paint::Solid { color } => TextFill::Solid(rgba8(*color)),
344 gradient => TextFill::Gradient(text_gradient(gradient)),
345 }
346}
347
348fn text_gradient(paint: &Paint) -> TextGradient {
349 let pair = |value: alpha2_model::Vec2| [value.x, value.y];
350 let stops = |stops: &[alpha2_model::GradientStop]| {
351 stops
352 .iter()
353 .map(|stop| TextGradientStop {
354 offset: stop.offset,
355 color: [
356 stop.color.red,
357 stop.color.green,
358 stop.color.blue,
359 stop.color.alpha,
360 ],
361 })
362 .collect()
363 };
364 let spread = |spread: Spread| match spread {
365 Spread::Pad => TextSpread::Pad,
366 Spread::Repeat => TextSpread::Repeat,
367 Spread::Reflect => TextSpread::Reflect,
368 };
369 match paint {
370 Paint::Solid { color } => TextGradient {
372 kind: TextGradientKind::Linear {
373 start: [0.0, 0.0],
374 end: [1.0, 0.0],
375 },
376 stops: stops(&[
377 alpha2_model::GradientStop {
378 offset: 0.0,
379 color: *color,
380 },
381 alpha2_model::GradientStop {
382 offset: 1.0,
383 color: *color,
384 },
385 ]),
386 spread: TextSpread::Pad,
387 },
388 Paint::LinearGradient {
389 start,
390 end,
391 stops: list,
392 spread: mode,
393 } => TextGradient {
394 kind: TextGradientKind::Linear {
395 start: pair(*start),
396 end: pair(*end),
397 },
398 stops: stops(list),
399 spread: spread(*mode),
400 },
401 Paint::RadialGradient {
402 center,
403 radius,
404 stops: list,
405 spread: mode,
406 } => TextGradient {
407 kind: TextGradientKind::Radial {
408 center: pair(*center),
409 radius: *radius,
410 },
411 stops: stops(list),
412 spread: spread(*mode),
413 },
414 Paint::ConicGradient {
415 center,
416 angle_degrees,
417 stops: list,
418 } => TextGradient {
419 kind: TextGradientKind::Conic {
420 center: pair(*center),
421 angle: angle_degrees.to_radians(),
422 },
423 stops: stops(list),
424 spread: TextSpread::Pad,
425 },
426 }
427}
428
429#[must_use]
433pub fn stroke_pixels(strokes: &[TextStroke], font_size: f32) -> Vec<alpha2_text::TextStroke> {
434 let mut reach = 0.0_f32;
435 let mut pixels = Vec::with_capacity(strokes.len());
436 for stroke in strokes {
437 let room = MAX_STROKE_PIXELS - reach;
438 if room <= 0.0 {
439 break;
440 }
441 let width = (stroke.width * font_size).clamp(f32::MIN_POSITIVE, room);
442 reach += width;
443 pixels.push(alpha2_text::TextStroke {
444 color: rgba8(stroke.color),
445 width,
446 });
447 }
448 pixels
449}
450
451#[derive(Clone, Debug, PartialEq)]
454pub struct TextDraw {
455 pub request: TextRasterRequest,
457 pub shadows: Vec<ShadowPixels>,
459 pub animation: Option<TextAnimation>,
461}
462
463#[derive(Clone, Debug, PartialEq)]
465pub struct TextAnimation {
466 pub animators: Vec<alpha2_eval::SampledAnimator>,
468 pub runs: Vec<alpha2_model::TextRun>,
470 pub time: alpha2_time::MediaTime,
472}
473
474impl TextDraw {
475 pub fn states(
481 &self,
482 rasterizer: &TextRasterizer,
483 ) -> Result<Option<(Vec<alpha2_text::ClusterState>, u64)>, TextRasterError> {
484 let Some(animation) = &self.animation else {
485 return Ok(None);
486 };
487 let layout = rasterizer.layout(&self.request)?;
488 Ok(Some(crate::text_animation::cluster_states(
489 &layout,
490 &animation.runs,
491 &animation.animators,
492 animation.time,
493 self.request.font_size,
494 )))
495 }
496}
497
498#[must_use]
502pub fn animated_text_key(
503 content: &alpha2_model::TextContent,
504 style: &TextStyle,
505 size: PictureSize,
506 time: alpha2_time::MediaTime,
507) -> u64 {
508 let key = text_key(&content.text(), style, size);
509 if style.animators.is_empty() {
510 return key;
511 }
512 let mut hash = key;
513 let mut feed = |bytes: &[u8]| {
514 for byte in bytes {
515 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
516 }
517 };
518 feed(&[0xfa]);
519 for run in content.runs() {
520 feed(&run.text.len().to_le_bytes());
521 for class in &run.class {
522 feed(class.as_bytes());
523 feed(&[0xff]);
524 }
525 if let Some(span) = run.time {
526 feed(&span.start.ticks().to_le_bytes());
527 feed(&span.end.ticks().to_le_bytes());
528 }
529 feed(&[0xfe]);
530 }
531 feed(&time.ticks().to_le_bytes());
532 hash.max(1)
533}
534
535#[must_use]
537pub fn text_draw(text: &str, style: &TextStyle, size: PictureSize, key: u64) -> TextDraw {
538 let request = text_request(text, style, size, key);
539 let shadows = shadow_pixels(&style.shadows, request.font_size);
540 TextDraw {
541 request,
542 shadows,
543 animation: None,
544 }
545}
546
547#[derive(Copy, Clone, Debug, PartialEq)]
549pub struct ShadowPixels {
550 pub offset: [f32; 2],
552 pub sigma: f32,
554 pub color: [f32; 4],
556}
557
558#[must_use]
561pub fn shadow_pixels(shadows: &[TextShadow], font_size: f32) -> Vec<ShadowPixels> {
562 shadows
563 .iter()
564 .map(|shadow| ShadowPixels {
565 offset: [shadow.offset.x * font_size, shadow.offset.y * font_size],
566 sigma: shadow.blur * font_size * 0.5,
567 color: [
568 shadow.color.red,
569 shadow.color.green,
570 shadow.color.blue,
571 shadow.color.alpha,
572 ],
573 })
574 .collect()
575}
576
577#[must_use]
579pub fn font_pixels(style: &TextStyle, size: PictureSize) -> f32 {
580 (style.font_size * size.unit()).clamp(1.0, MAX_FONT_PIXELS)
581}
582
583#[cfg(test)]
584#[path = "text_tests.rs"]
585mod tests;