1use alpha2_media::EmojiPolicy;
5use alpha2_model::{Rgba, TextAlign, TextStyle};
6use alpha2_text::{TextRasterError, TextRasterRequest, TextRasterizer};
7
8use crate::{Baselines, CanvasRect, PictureSize};
9
10pub const MAX_FONT_PIXELS: f32 = 2_048.0;
12pub const MAX_OUTLINE_PIXELS: f32 = 64.0;
14pub const MAX_PADDING_PIXELS: f32 = 256.0;
16
17#[derive(Copy, Clone, Debug, PartialEq)]
22pub struct TextBounds {
23 pub layout: CanvasRect,
27 pub content: CanvasRect,
32 pub ink: Option<CanvasRect>,
38 pub line_count: u32,
40 pub baselines: Baselines,
42}
43
44#[must_use]
47pub fn layout_text_key(request: &TextRasterRequest) -> 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 hash.max(1)
58}
59
60#[must_use]
63pub fn shaped_key(request: &TextRasterRequest) -> u64 {
64 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
65 let mut feed = |bytes: &[u8]| {
66 for byte in bytes {
67 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
68 }
69 };
70 feed(request.text.as_bytes());
71 feed(&[0xff]);
72 feed(
73 request
74 .font_family
75 .as_deref()
76 .unwrap_or_default()
77 .as_bytes(),
78 );
79 feed(&[0xff, u8::from(request.font_family.is_some())]);
80 feed(&request.font_size.to_bits().to_le_bytes());
81 feed(&request.line_height.to_bits().to_le_bytes());
82 feed(&request.letter_spacing.to_bits().to_le_bytes());
83 feed(&request.font_weight.to_le_bytes());
84 hash
85}
86
87fn feed_request(feed: &mut impl FnMut(&[u8]), request: &TextRasterRequest) {
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 for value in [
99 request.font_size,
100 request.max_width,
101 request.line_height,
102 request.letter_spacing,
103 ] {
104 feed(&value.to_bits().to_le_bytes());
105 }
106 feed(&request.font_weight.to_le_bytes());
107 feed(&request.color);
108 feed(&[match request.align {
109 alpha2_media::TextAlign::Start => 0,
110 alpha2_media::TextAlign::Center => 1,
111 alpha2_media::TextAlign::End => 2,
112 }]);
113 match request.outline {
114 Some(outline) => {
115 feed(&[1]);
116 feed(&outline.color);
117 feed(&outline.width.to_bits().to_le_bytes());
118 }
119 None => feed(&[0]),
120 }
121}
122
123#[must_use]
126pub fn text_key(text: &str, style: &TextStyle, size: PictureSize) -> u64 {
127 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
128 let mut feed = |bytes: &[u8]| {
129 for byte in bytes {
130 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
131 }
132 };
133 feed(text.as_bytes());
134 feed(&[0xff]);
135 feed(style.font_family.as_deref().unwrap_or_default().as_bytes());
137 feed(&[0xff, u8::from(style.font_family.is_some())]);
138 for value in [
139 style.font_size,
140 style.line_height,
141 style.letter_spacing,
142 style.max_width,
143 style.position.x,
144 style.position.y,
145 style.anchor.x,
146 style.anchor.y,
147 ] {
148 feed(&value.to_bits().to_le_bytes());
149 }
150 feed(&style.font_weight.to_le_bytes());
151 feed(&rgba8(style.color));
152 feed(&[match style.align {
153 TextAlign::Start => 0,
154 TextAlign::Center => 1,
155 TextAlign::End => 2,
156 }]);
157 match style.outline {
158 Some(outline) => {
159 feed(&[1]);
160 feed(&rgba8(outline.color));
161 feed(&outline.width.to_bits().to_le_bytes());
162 }
163 None => feed(&[0]),
164 }
165 match style.background {
166 Some(background) => {
167 feed(&[1]);
168 feed(&rgba8(background.color));
169 feed(&background.padding.to_bits().to_le_bytes());
170 }
171 None => feed(&[0]),
172 }
173 feed(&size.width().to_le_bytes());
174 feed(&size.height().to_le_bytes());
175 hash.max(1)
176}
177
178#[allow(clippy::cast_precision_loss)]
187pub fn measure_text(
188 rasterizer: &TextRasterizer,
189 text: &str,
190 style: &TextStyle,
191 size: PictureSize,
192) -> Result<TextBounds, TextRasterError> {
193 let request = text_request(text, style, size, text_key(text, style, size));
194 let measure = rasterizer.measure(&request)?;
195 let (width, height) = (size.width() as f32, size.height() as f32);
196 let margin = measure.decoration_margin as f32;
197 let (box_width, box_height) = (measure.box_width as f32, measure.box_height as f32);
198 let origin = [
201 style.position.x * width - style.anchor.x * (box_width + 2.0 * margin) + margin,
202 style.position.y * height - style.anchor.y * (box_height + 2.0 * margin) + margin,
203 ];
204 let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
205 left: (origin[0] + left) / width,
206 top: (origin[1] + top) / height,
207 right: (origin[0] + right) / width,
208 bottom: (origin[1] + bottom) / height,
209 };
210 Ok(TextBounds {
211 layout: rect([-margin, -margin, box_width + margin, box_height + margin]),
212 content: rect(measure.content),
213 ink: measure.ink.map(rect),
214 line_count: measure.line_count,
215 baselines: Baselines {
216 first: (origin[1] + measure.first_baseline) / height,
217 last: (origin[1] + measure.last_baseline) / height,
218 },
219 })
220}
221
222#[must_use]
224#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
225pub fn rgba8(color: Rgba) -> [u8; 4] {
226 [color.red, color.green, color.blue, color.alpha]
227 .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
228}
229
230#[must_use]
234#[allow(clippy::cast_precision_loss)]
235pub fn text_request(
236 text: &str,
237 style: &TextStyle,
238 size: PictureSize,
239 key: u64,
240) -> TextRasterRequest {
241 let font_size = (style.font_size * size.unit()).clamp(1.0, MAX_FONT_PIXELS);
242 TextRasterRequest {
243 text: text.to_owned(),
244 font_size,
245 max_width: (style.max_width * size.width() as f32).max(1.0),
246 line_height: style.line_height,
247 align: match style.align {
248 TextAlign::Start => alpha2_media::TextAlign::Start,
249 TextAlign::Center => alpha2_media::TextAlign::Center,
250 TextAlign::End => alpha2_media::TextAlign::End,
251 },
252 color: rgba8(style.color),
253 emoji_policy: EmojiPolicy::Color,
254 content_version: key,
255 outline: style.outline.map(|outline| alpha2_text::TextOutline {
256 color: rgba8(outline.color),
257 width: (outline.width * font_size).clamp(f32::MIN_POSITIVE, MAX_OUTLINE_PIXELS),
258 }),
259 background: style
260 .background
261 .map(|background| alpha2_text::TextBackground {
262 color: rgba8(background.color),
263 padding: (background.padding * font_size).clamp(0.0, MAX_PADDING_PIXELS),
264 }),
265 font_family: style.font_family.clone(),
266 letter_spacing: style.letter_spacing * font_size,
267 font_weight: style.font_weight,
268 }
269}
270
271#[cfg(test)]
272#[path = "text_tests.rs"]
273mod tests;