Skip to main content

pfx_text/
print.rs

1use crate::{Anchor, AtlasPage, Error, MipLevel, Representation, Span, TextEngine, caret, marks};
2
3const FULL_CLIP: [f32; 4] = [-f32::MAX, -f32::MAX, f32::MAX, f32::MAX];
4const FILL_GRID: u32 = 8;
5const ICON_GRID: u32 = 8;
6const MAX_TEXELS: u64 = 1 << 28;
7
8#[derive(Clone, Debug, PartialEq)]
9pub struct PrintImage {
10    pub width: u32,
11    pub height: u32,
12    pub density: f32,
13    pub pixels: Vec<u8>,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Selection {
18    pub from: usize,
19    pub to: usize,
20    pub color: [f32; 4],
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct Caret {
25    pub offset: usize,
26    pub color: [f32; 4],
27    pub width: f32,
28    pub height: f32,
29    pub shift: f32,
30}
31
32#[derive(Clone, Debug)]
33pub struct PrintBlock<'a> {
34    pub spans: &'a [Span],
35    pub wrap: Option<f32>,
36    pub anchor: Anchor,
37    pub at: [f32; 2],
38    pub alpha: f32,
39    pub clip: [f32; 4],
40    pub turn: [f32; 3],
41    pub selection: Option<Selection>,
42    pub caret: Option<Caret>,
43}
44
45impl<'a> PrintBlock<'a> {
46    pub fn new(spans: &'a [Span], at: [f32; 2]) -> Self {
47        Self {
48            spans,
49            wrap: None,
50            anchor: Anchor::Start,
51            at,
52            alpha: 1.0,
53            clip: FULL_CLIP,
54            turn: [0.0; 3],
55            selection: None,
56            caret: None,
57        }
58    }
59}
60
61#[derive(Clone, Copy, Debug)]
62pub struct Icon<'a> {
63    pub rect: [f32; 4],
64    pub width: u32,
65    pub height: u32,
66    pub rgba: &'a [u8],
67    pub premultiplied: bool,
68    pub tint: [f32; 4],
69    pub clip: [f32; 4],
70    pub turn: [f32; 3],
71}
72
73impl<'a> Icon<'a> {
74    pub fn new(rect: [f32; 4], width: u32, height: u32, rgba: &'a [u8]) -> Self {
75        Self {
76            rect,
77            width,
78            height,
79            rgba,
80            premultiplied: false,
81            tint: [1.0; 4],
82            clip: FULL_CLIP,
83            turn: [0.0; 3],
84        }
85    }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct Printed {
90    pub lines: usize,
91    pub width: f32,
92    pub height: f32,
93    pub baseline: f32,
94    pub origin: [f32; 2],
95    pub caret: Option<[f32; 4]>,
96}
97
98#[derive(Clone, Copy)]
99struct Frame {
100    clip: [f32; 4],
101    pivot: [f32; 2],
102    cos: f32,
103    sin: f32,
104    turned: bool,
105}
106
107impl Frame {
108    fn new(density: f32, clip: [f32; 4], turn: [f32; 3]) -> Self {
109        Self {
110            clip: clip.map(|value| value * density),
111            pivot: [turn[0] * density, turn[1] * density],
112            cos: turn[2].cos(),
113            sin: turn[2].sin(),
114            turned: turn[2] != 0.0,
115        }
116    }
117
118    fn bounds(&self, rect: [f32; 4]) -> [i32; 4] {
119        let corners = [
120            [rect[0], rect[1]],
121            [rect[0] + rect[2], rect[1]],
122            [rect[0], rect[1] + rect[3]],
123            [rect[0] + rect[2], rect[1] + rect[3]],
124        ];
125        let mut low = [f32::MAX; 2];
126        let mut high = [f32::MIN; 2];
127        for corner in corners {
128            let point = if self.turned {
129                let dx = corner[0] - self.pivot[0];
130                let dy = corner[1] - self.pivot[1];
131                [
132                    self.pivot[0] + self.cos * dx - self.sin * dy,
133                    self.pivot[1] + self.sin * dx + self.cos * dy,
134                ]
135            } else {
136                corner
137            };
138            for axis in 0..2 {
139                low[axis] = low[axis].min(point[axis]);
140                high[axis] = high[axis].max(point[axis]);
141            }
142        }
143        [
144            low[0].floor() as i32,
145            low[1].floor() as i32,
146            high[0].ceil() as i32,
147            high[1].ceil() as i32,
148        ]
149    }
150
151    fn source(&self, x: f32, y: f32) -> Option<[f32; 2]> {
152        let point = if self.turned {
153            let dx = x - self.pivot[0];
154            let dy = y - self.pivot[1];
155            [
156                self.pivot[0] + self.cos * dx + self.sin * dy,
157                self.pivot[1] - self.sin * dx + self.cos * dy,
158            ]
159        } else {
160            [x, y]
161        };
162        (point[0] >= self.clip[0]
163            && point[0] <= self.clip[2]
164            && point[1] >= self.clip[1]
165            && point[1] <= self.clip[3])
166            .then_some(point)
167    }
168}
169
170fn valid(density: f32, values: &[f32]) -> Result<(), Error> {
171    if !density.is_finite() || density <= 0.0 || values.iter().any(|value| !value.is_finite()) {
172        return Err(Error::InvalidStyle);
173    }
174    Ok(())
175}
176
177fn finite_clip(clip: [f32; 4]) -> [f32; 4] {
178    clip.map(|value| value.clamp(-f32::MAX, f32::MAX))
179}
180
181impl PrintImage {
182    pub fn new(width: f32, height: f32, density: f32) -> Result<Self, Error> {
183        valid(density, &[width, height])?;
184        if width <= 0.0 || height <= 0.0 {
185            return Err(Error::InvalidStyle);
186        }
187        let columns = (width * density).ceil();
188        let rows = (height * density).ceil();
189        if columns > u32::MAX as f32 || rows > u32::MAX as f32 {
190            return Err(Error::AtlasTooLarge);
191        }
192        Self::from_texels(columns as u32, rows as u32, density)
193    }
194
195    pub fn from_texels(width: u32, height: u32, density: f32) -> Result<Self, Error> {
196        valid(density, &[])?;
197        if width == 0 || height == 0 {
198            return Err(Error::InvalidStyle);
199        }
200        if u64::from(width) * u64::from(height) > MAX_TEXELS {
201            return Err(Error::AtlasTooLarge);
202        }
203        Ok(Self {
204            width,
205            height,
206            density,
207            pixels: vec![0; width as usize * height as usize * 4],
208        })
209    }
210
211    pub fn pixel(&self, x: u32, y: u32) -> [u8; 4] {
212        let at = ((y * self.width + x) * 4) as usize;
213        [
214            self.pixels[at],
215            self.pixels[at + 1],
216            self.pixels[at + 2],
217            self.pixels[at + 3],
218        ]
219    }
220
221    fn blend(&mut self, x: i32, y: i32, source: [f32; 4]) {
222        if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 || source[3] <= 0.0 {
223            return;
224        }
225        let at = ((y as u32 * self.width + x as u32) * 4) as usize;
226        let keep = 1.0 - source[3].min(1.0);
227        for (byte, value) in self.pixels[at..at + 4].iter_mut().zip(source) {
228            let below = f32::from(*byte) / 255.0;
229            let value = value.clamp(0.0, 1.0) + below * keep;
230            *byte = (value * 255.0).round().min(255.0) as u8;
231        }
232    }
233
234    pub fn fill(&mut self, rect: [f32; 4], color: [f32; 4], clip: [f32; 4], turn: [f32; 3]) {
235        let density = self.density;
236        if !rect.iter().chain(&color).all(|value| value.is_finite()) || color[3] <= 0.0 {
237            return;
238        }
239        let frame = Frame::new(density, finite_clip(clip), turn);
240        let rect = rect.map(|value| value * density);
241        let bounds = frame.bounds(rect);
242        let alpha = color[3].min(1.0);
243        let source = [color[0] * alpha, color[1] * alpha, color[2] * alpha, alpha];
244        for y in bounds[1]..bounds[3] {
245            for x in bounds[0]..bounds[2] {
246                let mut inside = 0u32;
247                for j in 0..FILL_GRID {
248                    for i in 0..FILL_GRID {
249                        let sx = x as f32 + (i as f32 + 0.5) / FILL_GRID as f32;
250                        let sy = y as f32 + (j as f32 + 0.5) / FILL_GRID as f32;
251                        if let Some(p) = frame.source(sx, sy)
252                            && p[0] >= rect[0]
253                            && p[0] < rect[0] + rect[2]
254                            && p[1] >= rect[1]
255                            && p[1] < rect[1] + rect[3]
256                        {
257                            inside += 1;
258                        }
259                    }
260                }
261                if inside > 0 {
262                    let cover = inside as f32 / (FILL_GRID * FILL_GRID) as f32;
263                    self.blend(x, y, source.map(|value| value * cover));
264                }
265            }
266        }
267    }
268
269    pub fn icon(&mut self, icon: &Icon<'_>) -> Result<(), Error> {
270        let density = self.density;
271        if icon.width == 0
272            || icon.height == 0
273            || icon.rgba.len() != icon.width as usize * icon.height as usize * 4
274            || !icon
275                .rect
276                .iter()
277                .chain(&icon.tint)
278                .all(|value| value.is_finite())
279            || icon.rect[2] <= 0.0
280            || icon.rect[3] <= 0.0
281        {
282            return Err(Error::InvalidStyle);
283        }
284        let frame = Frame::new(density, finite_clip(icon.clip), icon.turn);
285        let rect = icon.rect.map(|value| value * density);
286        let reach = (icon.width as f32 / rect[2])
287            .max(icon.height as f32 / rect[3])
288            .ceil()
289            .clamp(1.0, ICON_GRID as f32) as u32;
290        let bounds = frame.bounds(rect);
291        let texel = |x: i32, y: i32| -> [f32; 4] {
292            let at = ((y as u32 * icon.width + x as u32) * 4) as usize;
293            let mut rgba = [0.0; 4];
294            for (value, byte) in rgba.iter_mut().zip(&icon.rgba[at..at + 4]) {
295                *value = f32::from(*byte) / 255.0;
296            }
297            if !icon.premultiplied {
298                let alpha = rgba[3];
299                for value in &mut rgba[..3] {
300                    *value *= alpha;
301                }
302            }
303            rgba
304        };
305        for y in bounds[1]..bounds[3] {
306            for x in bounds[0]..bounds[2] {
307                let mut sum = [0.0f32; 4];
308                for j in 0..reach {
309                    for i in 0..reach {
310                        let sx = x as f32 + (i as f32 + 0.5) / reach as f32;
311                        let sy = y as f32 + (j as f32 + 0.5) / reach as f32;
312                        let Some(p) = frame.source(sx, sy) else {
313                            continue;
314                        };
315                        let u = (p[0] - rect[0]) / rect[2];
316                        let v = (p[1] - rect[1]) / rect[3];
317                        if !(0.0..1.0).contains(&u) || !(0.0..1.0).contains(&v) {
318                            continue;
319                        }
320                        let ax = u * icon.width as f32 - 0.5;
321                        let ay = v * icon.height as f32 - 0.5;
322                        let x0 = ax.floor();
323                        let y0 = ay.floor();
324                        let (tx, ty) = (ax - x0, ay - y0);
325                        let clamp_x = |value: f32| value.clamp(0.0, icon.width as f32 - 1.0) as i32;
326                        let clamp_y =
327                            |value: f32| value.clamp(0.0, icon.height as f32 - 1.0) as i32;
328                        let (x0i, x1i) = (clamp_x(x0), clamp_x(x0 + 1.0));
329                        let (y0i, y1i) = (clamp_y(y0), clamp_y(y0 + 1.0));
330                        let corners = [
331                            (texel(x0i, y0i), (1.0 - tx) * (1.0 - ty)),
332                            (texel(x1i, y0i), tx * (1.0 - ty)),
333                            (texel(x0i, y1i), (1.0 - tx) * ty),
334                            (texel(x1i, y1i), tx * ty),
335                        ];
336                        for (rgba, weight) in corners {
337                            for channel in 0..4 {
338                                sum[channel] += rgba[channel] * weight;
339                            }
340                        }
341                    }
342                }
343                let count = (reach * reach) as f32;
344                let alpha = icon.tint[3];
345                self.blend(
346                    x,
347                    y,
348                    [
349                        sum[0] / count * icon.tint[0] * alpha,
350                        sum[1] / count * icon.tint[1] * alpha,
351                        sum[2] / count * icon.tint[2] * alpha,
352                        sum[3] / count * alpha,
353                    ],
354                );
355            }
356        }
357        Ok(())
358    }
359}
360
361fn fetch(level: &MipLevel, channels: usize, cell: [i32; 4], x: f32, y: f32) -> [f32; 4] {
362    let x0 = x.floor();
363    let y0 = y.floor();
364    let (tx, ty) = (x - x0, y - y0);
365    let clamp_x = |value: f32| (value as i32).clamp(cell[0], cell[0] + cell[2] - 1) as usize;
366    let clamp_y = |value: f32| (value as i32).clamp(cell[1], cell[1] + cell[3] - 1) as usize;
367    let (x0i, x1i) = (clamp_x(x0), clamp_x(x0 + 1.0));
368    let (y0i, y1i) = (clamp_y(y0), clamp_y(y0 + 1.0));
369    let at = |px: usize, py: usize| -> [f32; 4] {
370        let base = (py * level.width as usize + px) * channels;
371        let mut out = [0.0; 4];
372        for (value, byte) in out.iter_mut().zip(&level.bytes[base..base + channels]) {
373            *value = f32::from(*byte) / 255.0;
374        }
375        out
376    };
377    let corners = [
378        (at(x0i, y0i), (1.0 - tx) * (1.0 - ty)),
379        (at(x1i, y0i), tx * (1.0 - ty)),
380        (at(x0i, y1i), (1.0 - tx) * ty),
381        (at(x1i, y1i), tx * ty),
382    ];
383    let mut out = [0.0; 4];
384    for (rgba, weight) in corners {
385        for channel in 0..4 {
386            out[channel] += rgba[channel] * weight;
387        }
388    }
389    out
390}
391
392impl TextEngine {
393    pub fn print_spans(
394        &mut self,
395        image: &mut PrintImage,
396        block: &PrintBlock<'_>,
397    ) -> Result<Printed, Error> {
398        let density = image.density;
399        valid(
400            density,
401            &[
402                block.at[0],
403                block.at[1],
404                block.alpha,
405                block.turn[0],
406                block.turn[1],
407                block.turn[2],
408            ],
409        )?;
410        let clip = finite_clip(block.clip);
411        let laid = self.layout_spans(block.spans, block.wrap, density, block.anchor)?;
412        let (width, height, baseline) = (laid.width, laid.height, laid.baseline);
413        let lines = laid.buffer.layout_runs().count().max(1);
414        let selection = block
415            .selection
416            .map(|selection| (marks(laid, selection.from, selection.to), selection.color));
417        let mark = block.caret.and_then(|caret_style| {
418            caret(laid, caret_style.offset).map(|rect| (caret_style, rect))
419        });
420        let origin = [block.at[0], block.at[1] - baseline];
421        let rich = self.render_spans(
422            block.spans,
423            block.wrap,
424            density,
425            block.anchor,
426            Representation::Coverage,
427            origin,
428            block.alpha,
429            clip,
430            block.turn,
431        )?;
432        if let Some((rects, color)) = selection {
433            for rect in rects {
434                image.fill(
435                    [origin[0] + rect[0], origin[1] + rect[1], rect[2], rect[3]],
436                    color,
437                    clip,
438                    block.turn,
439                );
440            }
441        }
442        let frame = Frame::new(density, clip, block.turn);
443        for quad in &rich.quads {
444            let (level, channels) = match quad.atlas_page {
445                AtlasPage::Monochrome => (&rich.atlas.levels[0], 1usize),
446                AtlasPage::Color => match &rich.color_atlas {
447                    Some(atlas) => (&atlas.levels[0], 4usize),
448                    None => continue,
449                },
450            };
451            let rect = quad.rect.map(|value| (value * density).round());
452            if rect[2] <= 0.0 || rect[3] <= 0.0 {
453                continue;
454            }
455            let cell = [
456                (quad.uv[0] * level.width as f32).round() as i32,
457                (quad.uv[1] * level.height as f32).round() as i32,
458                (quad.uv[2] * level.width as f32).round() as i32,
459                (quad.uv[3] * level.height as f32).round() as i32,
460            ];
461            let cell = [cell[0], cell[1], cell[2] - cell[0], cell[3] - cell[1]];
462            if cell[2] <= 0 || cell[3] <= 0 {
463                continue;
464            }
465            let tint = quad.color;
466            let bounds = frame.bounds(rect);
467            for y in bounds[1]..bounds[3] {
468                for x in bounds[0]..bounds[2] {
469                    let Some(p) = frame.source(x as f32 + 0.5, y as f32 + 0.5) else {
470                        continue;
471                    };
472                    let fx = (p[0] - rect[0]) / rect[2];
473                    let fy = (p[1] - rect[1]) / rect[3];
474                    if !(0.0..=1.0).contains(&fx) || !(0.0..=1.0).contains(&fy) {
475                        continue;
476                    }
477                    let ax = cell[0] as f32 + fx * cell[2] as f32 - 0.5;
478                    let ay = cell[1] as f32 + fy * cell[3] as f32 - 0.5;
479                    let sampled = fetch(level, channels, cell, ax, ay);
480                    let source = if channels == 1 {
481                        let opacity = sampled[0] * tint[3];
482                        [
483                            tint[0] * opacity,
484                            tint[1] * opacity,
485                            tint[2] * opacity,
486                            opacity,
487                        ]
488                    } else {
489                        [
490                            sampled[0] * tint[0] * tint[3],
491                            sampled[1] * tint[1] * tint[3],
492                            sampled[2] * tint[2] * tint[3],
493                            sampled[3] * tint[3],
494                        ]
495                    };
496                    image.blend(x, y, source);
497                }
498            }
499        }
500        let caret_rect = mark.map(|(style, rect)| {
501            let bar = [
502                origin[0] + rect[0] + style.shift,
503                origin[1] + rect[1],
504                style.width,
505                style.height,
506            ];
507            image.fill(bar, style.color, clip, block.turn);
508            bar
509        });
510        Ok(Printed {
511            lines,
512            width,
513            height,
514            baseline,
515            origin,
516            caret: caret_rect,
517        })
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use crate::Face;
525
526    const FONT: &[u8] = include_bytes!("../fonts/EBGaramond[wght].ttf");
527    const COLOR_FONT: &[u8] = include_bytes!("../tests/fonts/BungeeColor-Regular.ttf");
528
529    fn spans(text: &str, size: f32, color: [f32; 4]) -> Vec<Span> {
530        vec![Span {
531            text: text.into(),
532            face: Face {
533                family: "EB Garamond".into(),
534                size,
535                line: size * 1.3,
536                weight: 400,
537                italic: false,
538                spacing: 0.0,
539            },
540            color,
541        }]
542    }
543
544    fn reference_alpha(
545        engine: &mut TextEngine,
546        spans: &[Span],
547        wrap: Option<f32>,
548        density: f32,
549        origin: [f32; 2],
550        size: [u32; 2],
551    ) -> Vec<f32> {
552        let rich = engine
553            .render_spans(
554                spans,
555                wrap,
556                density,
557                Anchor::Start,
558                Representation::Coverage,
559                origin,
560                1.0,
561                FULL_CLIP,
562                [0.0; 3],
563            )
564            .unwrap();
565        let level = &rich.atlas.levels[0];
566        let mut alpha = vec![0.0f32; (size[0] * size[1]) as usize];
567        for quad in &rich.quads {
568            if quad.atlas_page != AtlasPage::Monochrome {
569                continue;
570            }
571            let rect = quad.rect.map(|value| (value * density).round() as i32);
572            let cell = [
573                (quad.uv[0] * level.width as f32).round() as i32,
574                (quad.uv[1] * level.height as f32).round() as i32,
575            ];
576            for dy in 0..rect[3] {
577                for dx in 0..rect[2] {
578                    let (x, y) = (rect[0] + dx, rect[1] + dy);
579                    if x < 0 || y < 0 || x >= size[0] as i32 || y >= size[1] as i32 {
580                        continue;
581                    }
582                    let byte = level.bytes
583                        [((cell[1] + dy) as u32 * level.width + (cell[0] + dx) as u32) as usize];
584                    let cover = f32::from(byte) / 255.0 * quad.color[3];
585                    let slot = &mut alpha[(y as u32 * size[0] + x as u32) as usize];
586                    *slot = 1.0 - (1.0 - *slot) * (1.0 - cover);
587                }
588            }
589        }
590        alpha
591    }
592
593    fn print(
594        engine: &mut TextEngine,
595        spans: &[Span],
596        edit: impl FnOnce(&mut PrintBlock<'_>),
597    ) -> (PrintImage, Printed) {
598        let mut image = PrintImage::new(60.0, 40.0, 2.0).unwrap();
599        let mut block = PrintBlock::new(spans, [4.0, 24.0]);
600        edit(&mut block);
601        let printed = engine.print_spans(&mut image, &block).unwrap();
602        (image, printed)
603    }
604
605    fn close(a: u8, b: u8, tolerance: i32) -> bool {
606        (i32::from(a) - i32::from(b)).abs() <= tolerance
607    }
608
609    #[test]
610    fn plain_text_matches_render_spans_coverage() {
611        let mut engine = TextEngine::new(FONT).unwrap();
612        let text = spans("Quiet paper, fine ink", 9.0, [0.0, 0.0, 0.0, 1.0]);
613        let (image, printed) = print(&mut engine, &text, |_| {});
614        let expected = reference_alpha(
615            &mut engine,
616            &text,
617            None,
618            2.0,
619            printed.origin,
620            [image.width, image.height],
621        );
622        let mut inked = 0;
623        for y in 0..image.height {
624            for x in 0..image.width {
625                let want = (expected[(y * image.width + x) as usize] * 255.0).round() as u8;
626                let got = image.pixel(x, y);
627                assert!(close(got[3], want, 1), "({x}, {y}): {} vs {want}", got[3]);
628                assert_eq!(got[..3], [0, 0, 0]);
629                inked += usize::from(got[3] > 0);
630            }
631        }
632        assert!(inked > 100);
633        assert_eq!(printed.lines, 1);
634        assert!(printed.width > 20.0);
635    }
636
637    #[test]
638    fn wrapped_text_counts_its_lines_and_matches_coverage() {
639        let mut engine = TextEngine::new(FONT).unwrap();
640        let text = spans(
641            "Quiet paper holds fine ink and a long line that must wrap",
642            5.0,
643            [0.0, 0.0, 0.0, 1.0],
644        );
645        let (image, printed) = print(&mut engine, &text, |block| block.wrap = Some(30.0));
646        let expected = reference_alpha(
647            &mut engine,
648            &text,
649            Some(30.0),
650            2.0,
651            printed.origin,
652            [image.width, image.height],
653        );
654        assert!(printed.lines >= 2);
655        for y in 0..image.height {
656            for x in 0..image.width {
657                let want = (expected[(y * image.width + x) as usize] * 255.0).round() as u8;
658                assert!(close(image.pixel(x, y)[3], want, 1));
659            }
660        }
661    }
662
663    #[test]
664    fn colour_is_premultiplied_by_the_span_density() {
665        let mut engine = TextEngine::new(FONT).unwrap();
666        let text = spans("Ink", 12.0, [0.8, 0.2, 0.1, 0.5]);
667        let (image, _) = print(&mut engine, &text, |_| {});
668        let mut peak = 0;
669        for y in 0..image.height {
670            for x in 0..image.width {
671                let [r, g, b, a] = image.pixel(x, y);
672                peak = peak.max(a);
673                let alpha = f32::from(a);
674                assert!(close(r, (alpha * 0.8).round() as u8, 1));
675                assert!(close(g, (alpha * 0.2).round() as u8, 1));
676                assert!(close(b, (alpha * 0.1).round() as u8, 1));
677            }
678        }
679        assert!((126..=129).contains(&peak), "{peak}");
680    }
681
682    #[test]
683    fn block_alpha_scales_the_print() {
684        let mut engine = TextEngine::new(FONT).unwrap();
685        let text = spans("Ink", 12.0, [0.0, 0.0, 0.0, 1.0]);
686        let (full, _) = print(&mut engine, &text, |_| {});
687        let (half, _) = print(&mut engine, &text, |block| block.alpha = 0.5);
688        for y in 0..full.height {
689            for x in 0..full.width {
690                let want = (f32::from(full.pixel(x, y)[3]) * 0.5).round() as u8;
691                assert!(close(half.pixel(x, y)[3], want, 1));
692            }
693        }
694    }
695
696    #[test]
697    fn clip_keeps_only_the_inside() {
698        let mut engine = TextEngine::new(FONT).unwrap();
699        let text = spans("Quiet paper, fine ink", 9.0, [0.0, 0.0, 0.0, 1.0]);
700        let (whole, _) = print(&mut engine, &text, |_| {});
701        let (clipped, _) = print(&mut engine, &text, |block| {
702            block.clip = [0.0, 0.0, 20.0, 40.0]
703        });
704        let mut kept = 0;
705        for y in 0..whole.height {
706            for x in 0..whole.width {
707                let got = clipped.pixel(x, y);
708                if x < 40 {
709                    assert_eq!(got, whole.pixel(x, y));
710                    kept += usize::from(got[3] > 0);
711                } else {
712                    assert_eq!(got, [0; 4], "({x}, {y})");
713                }
714            }
715        }
716        assert!(kept > 50);
717        assert!(whole.pixels.iter().skip(40 * 4).any(|&byte| byte > 0));
718    }
719
720    #[test]
721    fn a_quarter_turn_moves_every_texel() {
722        let mut engine = TextEngine::new(FONT).unwrap();
723        let text = spans("Quiet paper", 9.0, [0.0, 0.0, 0.0, 1.0]);
724        let mut upright = PrintImage::new(40.0, 40.0, 2.0).unwrap();
725        let mut turned = PrintImage::new(40.0, 40.0, 2.0).unwrap();
726        let block = PrintBlock::new(&text, [4.0, 24.0]);
727        engine.print_spans(&mut upright, &block).unwrap();
728        let mut spun = block.clone();
729        spun.turn = [20.0, 20.0, std::f32::consts::FRAC_PI_2];
730        engine.print_spans(&mut turned, &spun).unwrap();
731        let mut inked = 0;
732        for y in 0..80 {
733            for x in 0..80 {
734                let want = upright.pixel(x, y);
735                let got = turned.pixel(79 - y, x);
736                for channel in 0..4 {
737                    assert!(
738                        close(got[channel], want[channel], 1),
739                        "({x}, {y}) channel {channel}"
740                    );
741                }
742                inked += usize::from(want[3] > 0);
743            }
744        }
745        assert!(inked > 100);
746    }
747
748    #[test]
749    fn a_slight_turn_keeps_the_ink() {
750        let mut engine = TextEngine::new(FONT).unwrap();
751        let text = spans("Quiet paper", 9.0, [0.0, 0.0, 0.0, 1.0]);
752        let (upright, _) = print(&mut engine, &text, |_| {});
753        let (tilted, _) = print(&mut engine, &text, |block| block.turn = [4.0, 24.0, 0.1]);
754        let sum = |image: &PrintImage| {
755            image
756                .pixels
757                .chunks_exact(4)
758                .map(|texel| f32::from(texel[3]))
759                .sum::<f32>()
760        };
761        let ratio = sum(&tilted) / sum(&upright);
762        assert!((0.9..1.1).contains(&ratio), "{ratio}");
763        assert_ne!(tilted.pixels, upright.pixels);
764    }
765
766    #[test]
767    fn the_caret_sits_after_the_letter_and_the_selection_behind_the_words() {
768        let mut engine = TextEngine::new(FONT).unwrap();
769        let text = spans("hello world", 9.0, [0.0, 0.0, 0.0, 1.0]);
770        let (image, printed) = print(&mut engine, &text, |block| {
771            block.selection = Some(Selection {
772                from: 0,
773                to: 5,
774                color: [0.0, 0.0, 1.0, 1.0],
775            });
776            block.caret = Some(Caret {
777                offset: 11,
778                color: [1.0, 0.0, 0.0, 1.0],
779                width: 1.0,
780                height: 9.0,
781                shift: 0.5,
782            });
783        });
784        let laid = engine
785            .layout_spans(&text, None, 2.0, Anchor::Start)
786            .unwrap();
787        let last = marks(laid, 10, 11)[0];
788        let edge = last[0] + last[2];
789        let bar = printed.caret.unwrap();
790        assert!((bar[0] - (printed.origin[0] + edge + 0.5)).abs() < 1e-3);
791        let column = ((bar[0] + 0.5) * 2.0) as u32;
792        let row = ((bar[1] + 4.0) * 2.0) as u32;
793        assert_eq!(image.pixel(column, row), [255, 0, 0, 255]);
794        let selected = marks(laid, 0, 5)[0];
795        let top = ((printed.origin[1] + selected[1] + 0.5) * 2.0) as u32;
796        let left = ((printed.origin[0] + selected[0] + selected[2] * 0.5) * 2.0) as u32;
797        assert_eq!(image.pixel(left, top), [0, 0, 255, 255]);
798        let after = ((printed.origin[0] + selected[0] + selected[2] + 1.0) * 2.0) as u32;
799        assert_eq!(image.pixel(after, top)[3], 0);
800        let mut line = marks(laid, 0, 11);
801        assert_eq!(line.len(), 1);
802        let whole = line.remove(0);
803        assert!((caret(laid, 0).unwrap()[0] - whole[0]).abs() < 1e-3);
804        assert!((caret(laid, 11).unwrap()[0] - (whole[0] + whole[2])).abs() < 1e-3);
805    }
806
807    #[test]
808    fn carets_follow_wrapped_lines() {
809        let mut engine = TextEngine::new(FONT).unwrap();
810        let text = spans(
811            "Quiet paper holds fine ink and a long line that must wrap",
812            9.0,
813            [0.0, 0.0, 0.0, 1.0],
814        );
815        let laid = engine
816            .layout_spans(&text, Some(40.0), 1.0, Anchor::Start)
817            .unwrap();
818        let first = caret(laid, 0).unwrap();
819        let last = caret(laid, text[0].text.len()).unwrap();
820        assert!(last[1] > first[1]);
821        assert!(first[0] < 0.01);
822    }
823
824    #[test]
825    fn colour_glyphs_keep_their_colours() {
826        let mut engine = TextEngine::new(COLOR_FONT).unwrap();
827        let text = vec![Span {
828            text: "A".into(),
829            face: Face {
830                family: "Bungee Color".into(),
831                size: 20.0,
832                line: 24.0,
833                weight: 400,
834                italic: false,
835                spacing: 0.0,
836            },
837            color: [1.0; 4],
838        }];
839        let (image, _) = print(&mut engine, &text, |_| {});
840        let mut coloured = 0;
841        for texel in image.pixels.chunks_exact(4) {
842            assert!(texel[..3].iter().all(|&c| c <= texel[3] + 1), "{texel:?}");
843            coloured += usize::from(texel[3] > 0 && texel[0] != texel[1]);
844        }
845        assert!(coloured > 20);
846    }
847
848    #[test]
849    fn icons_land_at_their_texels_with_tint() {
850        let mut image = PrintImage::from_texels(6, 6, 1.0).unwrap();
851        let rgba = [
852            255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 128,
853        ];
854        let mut icon = Icon::new([2.0, 2.0, 2.0, 2.0], 2, 2, &rgba);
855        image.icon(&icon).unwrap();
856        assert_eq!(image.pixel(2, 2), [255, 0, 0, 255]);
857        assert_eq!(image.pixel(3, 2), [0, 255, 0, 255]);
858        assert_eq!(image.pixel(2, 3), [0, 0, 255, 255]);
859        assert_eq!(image.pixel(3, 3), [128, 128, 128, 128]);
860        assert_eq!(image.pixel(1, 2), [0; 4]);
861        assert_eq!(image.pixel(4, 3), [0; 4]);
862        let mut tinted = PrintImage::from_texels(6, 6, 1.0).unwrap();
863        icon.tint = [1.0, 1.0, 1.0, 0.5];
864        tinted.icon(&icon).unwrap();
865        assert_eq!(tinted.pixel(2, 2), [128, 0, 0, 128]);
866        assert!(image.icon(&Icon::new([0.0; 4], 2, 2, &rgba)).is_err());
867        assert!(
868            image
869                .icon(&Icon::new([0.0, 0.0, 1.0, 1.0], 2, 2, &rgba[..8]))
870                .is_err()
871        );
872    }
873
874    #[test]
875    fn fills_cover_fractions_of_texels() {
876        let mut image = PrintImage::from_texels(4, 4, 2.0).unwrap();
877        image.fill(
878            [0.25, 0.0, 1.0, 1.0],
879            [0.0, 0.0, 0.0, 1.0],
880            FULL_CLIP,
881            [0.0; 3],
882        );
883        assert_eq!(image.pixel(0, 0)[3], 128);
884        assert_eq!(image.pixel(1, 0)[3], 255);
885        assert_eq!(image.pixel(2, 0)[3], 128);
886        assert_eq!(image.pixel(3, 0)[3], 0);
887    }
888
889    #[test]
890    fn bad_inputs_are_refused() {
891        assert!(PrintImage::new(10.0, 10.0, 0.0).is_err());
892        assert!(PrintImage::new(0.0, 10.0, 1.0).is_err());
893        assert!(PrintImage::new(f32::NAN, 10.0, 1.0).is_err());
894        assert!(PrintImage::from_texels(0, 4, 1.0).is_err());
895        assert!(PrintImage::from_texels(100_000, 100_000, 1.0).is_err());
896        let mut engine = TextEngine::new(FONT).unwrap();
897        let text = spans("x", 9.0, [0.0, 0.0, 0.0, 1.0]);
898        let mut image = PrintImage::new(10.0, 10.0, 1.0).unwrap();
899        let mut block = PrintBlock::new(&text, [f32::NAN, 0.0]);
900        assert!(engine.print_spans(&mut image, &block).is_err());
901        block.at = [0.0, 5.0];
902        assert!(engine.print_spans(&mut image, &block).is_ok());
903    }
904}